nc1709 1.15.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. nc1709/__init__.py +13 -0
  2. nc1709/agent/__init__.py +36 -0
  3. nc1709/agent/core.py +505 -0
  4. nc1709/agent/mcp_bridge.py +245 -0
  5. nc1709/agent/permissions.py +298 -0
  6. nc1709/agent/tools/__init__.py +21 -0
  7. nc1709/agent/tools/base.py +440 -0
  8. nc1709/agent/tools/bash_tool.py +367 -0
  9. nc1709/agent/tools/file_tools.py +454 -0
  10. nc1709/agent/tools/notebook_tools.py +516 -0
  11. nc1709/agent/tools/search_tools.py +322 -0
  12. nc1709/agent/tools/task_tool.py +284 -0
  13. nc1709/agent/tools/web_tools.py +555 -0
  14. nc1709/agents/__init__.py +17 -0
  15. nc1709/agents/auto_fix.py +506 -0
  16. nc1709/agents/test_generator.py +507 -0
  17. nc1709/checkpoints.py +372 -0
  18. nc1709/cli.py +3380 -0
  19. nc1709/cli_ui.py +1080 -0
  20. nc1709/cognitive/__init__.py +149 -0
  21. nc1709/cognitive/anticipation.py +594 -0
  22. nc1709/cognitive/context_engine.py +1046 -0
  23. nc1709/cognitive/council.py +824 -0
  24. nc1709/cognitive/learning.py +761 -0
  25. nc1709/cognitive/router.py +583 -0
  26. nc1709/cognitive/system.py +519 -0
  27. nc1709/config.py +155 -0
  28. nc1709/custom_commands.py +300 -0
  29. nc1709/executor.py +333 -0
  30. nc1709/file_controller.py +354 -0
  31. nc1709/git_integration.py +308 -0
  32. nc1709/github_integration.py +477 -0
  33. nc1709/image_input.py +446 -0
  34. nc1709/linting.py +519 -0
  35. nc1709/llm_adapter.py +667 -0
  36. nc1709/logger.py +192 -0
  37. nc1709/mcp/__init__.py +18 -0
  38. nc1709/mcp/client.py +370 -0
  39. nc1709/mcp/manager.py +407 -0
  40. nc1709/mcp/protocol.py +210 -0
  41. nc1709/mcp/server.py +473 -0
  42. nc1709/memory/__init__.py +20 -0
  43. nc1709/memory/embeddings.py +325 -0
  44. nc1709/memory/indexer.py +474 -0
  45. nc1709/memory/sessions.py +432 -0
  46. nc1709/memory/vector_store.py +451 -0
  47. nc1709/models/__init__.py +86 -0
  48. nc1709/models/detector.py +377 -0
  49. nc1709/models/formats.py +315 -0
  50. nc1709/models/manager.py +438 -0
  51. nc1709/models/registry.py +497 -0
  52. nc1709/performance/__init__.py +343 -0
  53. nc1709/performance/cache.py +705 -0
  54. nc1709/performance/pipeline.py +611 -0
  55. nc1709/performance/tiering.py +543 -0
  56. nc1709/plan_mode.py +362 -0
  57. nc1709/plugins/__init__.py +17 -0
  58. nc1709/plugins/agents/__init__.py +18 -0
  59. nc1709/plugins/agents/django_agent.py +912 -0
  60. nc1709/plugins/agents/docker_agent.py +623 -0
  61. nc1709/plugins/agents/fastapi_agent.py +887 -0
  62. nc1709/plugins/agents/git_agent.py +731 -0
  63. nc1709/plugins/agents/nextjs_agent.py +867 -0
  64. nc1709/plugins/base.py +359 -0
  65. nc1709/plugins/manager.py +411 -0
  66. nc1709/plugins/registry.py +337 -0
  67. nc1709/progress.py +443 -0
  68. nc1709/prompts/__init__.py +22 -0
  69. nc1709/prompts/agent_system.py +180 -0
  70. nc1709/prompts/task_prompts.py +340 -0
  71. nc1709/prompts/unified_prompt.py +133 -0
  72. nc1709/reasoning_engine.py +541 -0
  73. nc1709/remote_client.py +266 -0
  74. nc1709/shell_completions.py +349 -0
  75. nc1709/slash_commands.py +649 -0
  76. nc1709/task_classifier.py +408 -0
  77. nc1709/version_check.py +177 -0
  78. nc1709/web/__init__.py +8 -0
  79. nc1709/web/server.py +950 -0
  80. nc1709/web/templates/index.html +1127 -0
  81. nc1709-1.15.4.dist-info/METADATA +858 -0
  82. nc1709-1.15.4.dist-info/RECORD +86 -0
  83. nc1709-1.15.4.dist-info/WHEEL +5 -0
  84. nc1709-1.15.4.dist-info/entry_points.txt +2 -0
  85. nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
  86. nc1709-1.15.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,354 @@
1
+ """
2
+ Filesystem Controller with Safety Features
3
+ Handles all file operations with automatic backups and atomic transactions
4
+ """
5
+ import os
6
+ import shutil
7
+ import hashlib
8
+ import difflib
9
+ from pathlib import Path
10
+ from typing import List, Optional, Tuple
11
+ from datetime import datetime
12
+
13
+ from .config import get_config
14
+
15
+
16
+ class FileController:
17
+ """Manages safe filesystem operations"""
18
+
19
+ def __init__(self):
20
+ """Initialize the file controller"""
21
+ self.config = get_config()
22
+ # Get backup directory from config, with fallback to default
23
+ backup_path = self.config.get("safety.backup_dir", "~/.nc1709/backups")
24
+ self.backup_dir = Path(backup_path).expanduser().resolve()
25
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
26
+
27
+ def read_file(self, file_path: str) -> str:
28
+ """Read a file safely
29
+
30
+ Args:
31
+ file_path: Path to the file
32
+
33
+ Returns:
34
+ File contents as string
35
+
36
+ Raises:
37
+ FileNotFoundError: If file doesn't exist
38
+ PermissionError: If file can't be read
39
+ """
40
+ path = Path(file_path).expanduser().resolve()
41
+
42
+ if not path.exists():
43
+ raise FileNotFoundError(f"File not found: {file_path}")
44
+
45
+ if not path.is_file():
46
+ raise ValueError(f"Not a file: {file_path}")
47
+
48
+ try:
49
+ with open(path, 'r', encoding='utf-8') as f:
50
+ return f.read()
51
+ except UnicodeDecodeError:
52
+ # Try reading as binary if UTF-8 fails
53
+ with open(path, 'rb') as f:
54
+ return f.read().decode('utf-8', errors='replace')
55
+
56
+ def write_file(
57
+ self,
58
+ file_path: str,
59
+ content: str,
60
+ create_backup: bool = True,
61
+ confirm: bool = True
62
+ ) -> bool:
63
+ """Write content to a file with safety checks
64
+
65
+ Args:
66
+ file_path: Path to the file
67
+ content: Content to write
68
+ create_backup: Whether to create a backup first
69
+ confirm: Whether to ask for confirmation
70
+
71
+ Returns:
72
+ True if write was successful
73
+
74
+ Raises:
75
+ PermissionError: If file can't be written
76
+ """
77
+ path = Path(file_path).expanduser().resolve()
78
+
79
+ # Check if we should confirm
80
+ if confirm and self.config.get("safety.confirm_writes", True):
81
+ if path.exists():
82
+ print(f"\n⚠️ File exists: {path}")
83
+ print("This will overwrite the existing file.")
84
+ response = input("Continue? [y/N]: ").strip().lower()
85
+ if response != 'y':
86
+ print("Write cancelled.")
87
+ return False
88
+
89
+ # Create backup if file exists
90
+ if create_backup and path.exists() and self.config.get("safety.auto_backup", True):
91
+ self._create_backup(path)
92
+
93
+ # Ensure parent directory exists
94
+ path.parent.mkdir(parents=True, exist_ok=True)
95
+
96
+ # Write the file
97
+ try:
98
+ with open(path, 'w', encoding='utf-8') as f:
99
+ f.write(content)
100
+ print(f"✅ File written: {path}")
101
+ return True
102
+ except Exception as e:
103
+ print(f"❌ Failed to write file: {e}")
104
+ return False
105
+
106
+ def edit_file(
107
+ self,
108
+ file_path: str,
109
+ old_content: str,
110
+ new_content: str,
111
+ create_backup: bool = True
112
+ ) -> bool:
113
+ """Edit a file by replacing old content with new content
114
+
115
+ Args:
116
+ file_path: Path to the file
117
+ old_content: Content to replace
118
+ new_content: New content
119
+ create_backup: Whether to create a backup first
120
+
121
+ Returns:
122
+ True if edit was successful
123
+ """
124
+ path = Path(file_path).expanduser().resolve()
125
+
126
+ if not path.exists():
127
+ raise FileNotFoundError(f"File not found: {file_path}")
128
+
129
+ # Read current content
130
+ current_content = self.read_file(file_path)
131
+
132
+ # Check if old_content exists in file
133
+ if old_content not in current_content:
134
+ print(f"⚠️ Content to replace not found in {file_path}")
135
+ return False
136
+
137
+ # Create backup
138
+ if create_backup and self.config.get("safety.auto_backup", True):
139
+ self._create_backup(path)
140
+
141
+ # Replace content
142
+ updated_content = current_content.replace(old_content, new_content, 1)
143
+
144
+ # Show diff
145
+ self._show_diff(current_content, updated_content, str(path))
146
+
147
+ # Confirm
148
+ if self.config.get("safety.confirm_writes", True):
149
+ response = input("\nApply these changes? [y/N]: ").strip().lower()
150
+ if response != 'y':
151
+ print("Edit cancelled.")
152
+ return False
153
+
154
+ # Write updated content
155
+ return self.write_file(file_path, updated_content, create_backup=False, confirm=False)
156
+
157
+ def create_file(self, file_path: str, content: str = "") -> bool:
158
+ """Create a new file
159
+
160
+ Args:
161
+ file_path: Path to the file
162
+ content: Initial content (default: empty)
163
+
164
+ Returns:
165
+ True if creation was successful
166
+ """
167
+ path = Path(file_path).expanduser().resolve()
168
+
169
+ if path.exists():
170
+ print(f"⚠️ File already exists: {file_path}")
171
+ return False
172
+
173
+ return self.write_file(file_path, content, create_backup=False, confirm=False)
174
+
175
+ def delete_file(self, file_path: str, confirm: bool = True) -> bool:
176
+ """Delete a file with confirmation
177
+
178
+ Args:
179
+ file_path: Path to the file
180
+ confirm: Whether to ask for confirmation
181
+
182
+ Returns:
183
+ True if deletion was successful
184
+ """
185
+ path = Path(file_path).expanduser().resolve()
186
+
187
+ if not path.exists():
188
+ print(f"⚠️ File not found: {file_path}")
189
+ return False
190
+
191
+ # Create backup before deleting
192
+ if self.config.get("safety.auto_backup", True):
193
+ self._create_backup(path)
194
+
195
+ # Confirm deletion
196
+ if confirm and self.config.get("safety.confirm_destructive", True):
197
+ print(f"\n⚠️ About to delete: {path}")
198
+ response = input("Are you sure? [y/N]: ").strip().lower()
199
+ if response != 'y':
200
+ print("Deletion cancelled.")
201
+ return False
202
+
203
+ try:
204
+ path.unlink()
205
+ print(f"✅ File deleted: {path}")
206
+ return True
207
+ except Exception as e:
208
+ print(f"❌ Failed to delete file: {e}")
209
+ return False
210
+
211
+ def list_files(self, directory: str, pattern: str = "*") -> List[str]:
212
+ """List files in a directory
213
+
214
+ Args:
215
+ directory: Directory path
216
+ pattern: Glob pattern (default: all files)
217
+
218
+ Returns:
219
+ List of file paths
220
+ """
221
+ path = Path(directory).expanduser().resolve()
222
+
223
+ if not path.exists():
224
+ raise FileNotFoundError(f"Directory not found: {directory}")
225
+
226
+ if not path.is_dir():
227
+ raise ValueError(f"Not a directory: {directory}")
228
+
229
+ return [str(p) for p in path.glob(pattern) if p.is_file()]
230
+
231
+ def get_file_info(self, file_path: str) -> dict:
232
+ """Get information about a file
233
+
234
+ Args:
235
+ file_path: Path to the file
236
+
237
+ Returns:
238
+ Dictionary with file information
239
+ """
240
+ path = Path(file_path).expanduser().resolve()
241
+
242
+ if not path.exists():
243
+ raise FileNotFoundError(f"File not found: {file_path}")
244
+
245
+ stat = path.stat()
246
+
247
+ return {
248
+ "path": str(path),
249
+ "name": path.name,
250
+ "size": stat.st_size,
251
+ "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
252
+ "is_file": path.is_file(),
253
+ "is_dir": path.is_dir(),
254
+ "extension": path.suffix
255
+ }
256
+
257
+ def _create_backup(self, file_path: Path) -> Path:
258
+ """Create a backup of a file
259
+
260
+ Args:
261
+ file_path: Path to the file
262
+
263
+ Returns:
264
+ Path to the backup file
265
+ """
266
+ # Generate backup filename with timestamp
267
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
268
+ file_hash = hashlib.md5(str(file_path).encode()).hexdigest()[:8]
269
+ backup_name = f"{file_path.name}.{timestamp}.{file_hash}.backup"
270
+ backup_path = self.backup_dir / backup_name
271
+
272
+ # Copy file to backup
273
+ shutil.copy2(file_path, backup_path)
274
+
275
+ return backup_path
276
+
277
+ def _show_diff(self, old_content: str, new_content: str, filename: str) -> None:
278
+ """Show a diff between old and new content
279
+
280
+ Args:
281
+ old_content: Original content
282
+ new_content: New content
283
+ filename: Name of the file (for display)
284
+ """
285
+ old_lines = old_content.splitlines(keepends=True)
286
+ new_lines = new_content.splitlines(keepends=True)
287
+
288
+ diff = difflib.unified_diff(
289
+ old_lines,
290
+ new_lines,
291
+ fromfile=f"{filename} (original)",
292
+ tofile=f"{filename} (modified)",
293
+ lineterm=''
294
+ )
295
+
296
+ print("\n" + "="*60)
297
+ print("PROPOSED CHANGES:")
298
+ print("="*60)
299
+ for line in diff:
300
+ if line.startswith('+') and not line.startswith('+++'):
301
+ print(f"\033[92m{line}\033[0m", end='') # Green for additions
302
+ elif line.startswith('-') and not line.startswith('---'):
303
+ print(f"\033[91m{line}\033[0m", end='') # Red for deletions
304
+ else:
305
+ print(line, end='')
306
+ print("="*60)
307
+
308
+ def restore_from_backup(self, backup_path: str, target_path: Optional[str] = None) -> bool:
309
+ """Restore a file from backup
310
+
311
+ Args:
312
+ backup_path: Path to the backup file
313
+ target_path: Target path (default: original location)
314
+
315
+ Returns:
316
+ True if restore was successful
317
+ """
318
+ backup = Path(backup_path)
319
+
320
+ if not backup.exists():
321
+ print(f"⚠️ Backup not found: {backup_path}")
322
+ return False
323
+
324
+ if target_path is None:
325
+ # Extract original filename from backup name
326
+ original_name = backup.name.split('.')[0]
327
+ target_path = Path.cwd() / original_name
328
+ else:
329
+ target_path = Path(target_path)
330
+
331
+ try:
332
+ shutil.copy2(backup, target_path)
333
+ print(f"✅ File restored from backup: {target_path}")
334
+ return True
335
+ except Exception as e:
336
+ print(f"❌ Failed to restore from backup: {e}")
337
+ return False
338
+
339
+ def list_backups(self) -> List[Tuple[str, datetime]]:
340
+ """List all available backups
341
+
342
+ Returns:
343
+ List of (backup_path, timestamp) tuples
344
+ """
345
+ backups = []
346
+ for backup_file in self.backup_dir.glob("*.backup"):
347
+ stat = backup_file.stat()
348
+ timestamp = datetime.fromtimestamp(stat.st_mtime)
349
+ backups.append((str(backup_file), timestamp))
350
+
351
+ # Sort by timestamp (newest first)
352
+ backups.sort(key=lambda x: x[1], reverse=True)
353
+
354
+ return backups
@@ -0,0 +1,308 @@
1
+ """
2
+ Git Integration for NC1709
3
+
4
+ Provides automatic git commits and git-related utilities.
5
+ Similar to Aider's git integration.
6
+ """
7
+
8
+ import os
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Optional, List, Dict, Any
12
+ from dataclasses import dataclass
13
+ from datetime import datetime
14
+
15
+
16
+ @dataclass
17
+ class GitStatus:
18
+ """Status of a git repository"""
19
+ is_repo: bool
20
+ branch: str
21
+ has_changes: bool
22
+ staged_files: List[str]
23
+ modified_files: List[str]
24
+ untracked_files: List[str]
25
+
26
+
27
+ class GitIntegration:
28
+ """
29
+ Git integration for automatic commits and git operations.
30
+
31
+ Features:
32
+ - Automatic commits after file changes
33
+ - Smart commit message generation
34
+ - Git status and diff display
35
+ """
36
+
37
+ def __init__(self, repo_path: Optional[str] = None, auto_commit: bool = True):
38
+ """
39
+ Initialize git integration.
40
+
41
+ Args:
42
+ repo_path: Path to git repository (defaults to cwd)
43
+ auto_commit: Whether to auto-commit changes
44
+ """
45
+ self.repo_path = Path(repo_path) if repo_path else Path.cwd()
46
+ self.auto_commit = auto_commit
47
+ self._is_repo = self._check_git_repo()
48
+
49
+ def _check_git_repo(self) -> bool:
50
+ """Check if current directory is a git repository"""
51
+ try:
52
+ result = subprocess.run(
53
+ ["git", "rev-parse", "--git-dir"],
54
+ cwd=str(self.repo_path),
55
+ capture_output=True,
56
+ text=True
57
+ )
58
+ return result.returncode == 0
59
+ except (subprocess.SubprocessError, FileNotFoundError):
60
+ return False
61
+
62
+ @property
63
+ def is_repo(self) -> bool:
64
+ """Check if we're in a git repository"""
65
+ return self._is_repo
66
+
67
+ def _run_git(self, *args, check: bool = True) -> subprocess.CompletedProcess:
68
+ """Run a git command"""
69
+ return subprocess.run(
70
+ ["git"] + list(args),
71
+ cwd=str(self.repo_path),
72
+ capture_output=True,
73
+ text=True,
74
+ check=check
75
+ )
76
+
77
+ def get_status(self) -> GitStatus:
78
+ """Get current git status"""
79
+ if not self._is_repo:
80
+ return GitStatus(
81
+ is_repo=False,
82
+ branch="",
83
+ has_changes=False,
84
+ staged_files=[],
85
+ modified_files=[],
86
+ untracked_files=[]
87
+ )
88
+
89
+ # Get branch name
90
+ try:
91
+ result = self._run_git("branch", "--show-current")
92
+ branch = result.stdout.strip()
93
+ except subprocess.CalledProcessError:
94
+ branch = "unknown"
95
+
96
+ # Get status --porcelain
97
+ result = self._run_git("status", "--porcelain", check=False)
98
+ lines = result.stdout.strip().split("\n") if result.stdout.strip() else []
99
+
100
+ staged_files = []
101
+ modified_files = []
102
+ untracked_files = []
103
+
104
+ for line in lines:
105
+ if len(line) < 3:
106
+ continue
107
+ index_status = line[0]
108
+ worktree_status = line[1]
109
+ filename = line[3:]
110
+
111
+ if index_status in "MADRC":
112
+ staged_files.append(filename)
113
+ if worktree_status == "M":
114
+ modified_files.append(filename)
115
+ if index_status == "?" and worktree_status == "?":
116
+ untracked_files.append(filename)
117
+
118
+ return GitStatus(
119
+ is_repo=True,
120
+ branch=branch,
121
+ has_changes=bool(staged_files or modified_files or untracked_files),
122
+ staged_files=staged_files,
123
+ modified_files=modified_files,
124
+ untracked_files=untracked_files
125
+ )
126
+
127
+ def get_diff(self, staged: bool = False) -> str:
128
+ """Get git diff"""
129
+ if not self._is_repo:
130
+ return ""
131
+
132
+ args = ["diff"]
133
+ if staged:
134
+ args.append("--staged")
135
+
136
+ result = self._run_git(*args, check=False)
137
+ return result.stdout
138
+
139
+ def stage_file(self, file_path: str) -> bool:
140
+ """Stage a file for commit"""
141
+ if not self._is_repo:
142
+ return False
143
+
144
+ try:
145
+ self._run_git("add", file_path)
146
+ return True
147
+ except subprocess.CalledProcessError:
148
+ return False
149
+
150
+ def stage_all(self) -> bool:
151
+ """Stage all changes"""
152
+ if not self._is_repo:
153
+ return False
154
+
155
+ try:
156
+ self._run_git("add", "-A")
157
+ return True
158
+ except subprocess.CalledProcessError:
159
+ return False
160
+
161
+ def commit(self, message: str, files: Optional[List[str]] = None) -> bool:
162
+ """
163
+ Create a commit.
164
+
165
+ Args:
166
+ message: Commit message
167
+ files: Specific files to commit (stages them first)
168
+
169
+ Returns:
170
+ True if commit was successful
171
+ """
172
+ if not self._is_repo:
173
+ return False
174
+
175
+ try:
176
+ # Stage specific files if provided
177
+ if files:
178
+ for f in files:
179
+ self.stage_file(f)
180
+
181
+ # Check if there's anything to commit
182
+ status = self.get_status()
183
+ if not status.staged_files:
184
+ return False
185
+
186
+ # Create commit
187
+ self._run_git("commit", "-m", message)
188
+ return True
189
+
190
+ except subprocess.CalledProcessError:
191
+ return False
192
+
193
+ def auto_commit_files(
194
+ self,
195
+ files: List[str],
196
+ tool_name: str = "NC1709",
197
+ description: str = ""
198
+ ) -> Optional[str]:
199
+ """
200
+ Automatically commit changed files with a generated message.
201
+
202
+ Args:
203
+ files: List of file paths that were modified
204
+ tool_name: Name of the tool that made the change
205
+ description: Optional description of what changed
206
+
207
+ Returns:
208
+ Commit hash if successful, None otherwise
209
+ """
210
+ if not self._is_repo or not self.auto_commit:
211
+ return None
212
+
213
+ # Filter to files that actually have changes
214
+ changed_files = []
215
+ for f in files:
216
+ result = self._run_git("status", "--porcelain", f, check=False)
217
+ if result.stdout.strip():
218
+ changed_files.append(f)
219
+
220
+ if not changed_files:
221
+ return None
222
+
223
+ # Generate commit message
224
+ file_names = [Path(f).name for f in changed_files]
225
+ if len(file_names) == 1:
226
+ file_desc = file_names[0]
227
+ elif len(file_names) <= 3:
228
+ file_desc = ", ".join(file_names)
229
+ else:
230
+ file_desc = f"{file_names[0]} and {len(file_names) - 1} other files"
231
+
232
+ if description:
233
+ message = f"{description}\n\nModified: {file_desc}\n\n[{tool_name}]"
234
+ else:
235
+ message = f"Update {file_desc}\n\n[{tool_name}]"
236
+
237
+ # Stage and commit
238
+ for f in changed_files:
239
+ self.stage_file(f)
240
+
241
+ try:
242
+ self._run_git("commit", "-m", message)
243
+
244
+ # Get the commit hash
245
+ result = self._run_git("rev-parse", "--short", "HEAD")
246
+ return result.stdout.strip()
247
+
248
+ except subprocess.CalledProcessError:
249
+ return None
250
+
251
+ def get_recent_commits(self, limit: int = 10) -> List[Dict[str, Any]]:
252
+ """Get recent commit history"""
253
+ if not self._is_repo:
254
+ return []
255
+
256
+ try:
257
+ result = self._run_git(
258
+ "log",
259
+ f"-{limit}",
260
+ "--pretty=format:%h|%s|%an|%ar",
261
+ check=False
262
+ )
263
+
264
+ commits = []
265
+ for line in result.stdout.strip().split("\n"):
266
+ if "|" in line:
267
+ parts = line.split("|", 3)
268
+ if len(parts) >= 4:
269
+ commits.append({
270
+ "hash": parts[0],
271
+ "message": parts[1],
272
+ "author": parts[2],
273
+ "time_ago": parts[3]
274
+ })
275
+
276
+ return commits
277
+
278
+ except subprocess.CalledProcessError:
279
+ return []
280
+
281
+ def init_repo(self) -> bool:
282
+ """Initialize a new git repository"""
283
+ try:
284
+ self._run_git("init")
285
+ self._is_repo = True
286
+ return True
287
+ except subprocess.CalledProcessError:
288
+ return False
289
+
290
+
291
+ # Global git integration instance
292
+ _git_integration: Optional[GitIntegration] = None
293
+
294
+
295
+ def get_git_integration(auto_commit: bool = True) -> GitIntegration:
296
+ """Get or create the global git integration"""
297
+ global _git_integration
298
+ if _git_integration is None:
299
+ _git_integration = GitIntegration(auto_commit=auto_commit)
300
+ return _git_integration
301
+
302
+
303
+ def auto_commit_after_edit(file_path: str, tool_name: str = "Edit") -> Optional[str]:
304
+ """Convenience function to auto-commit after editing a file"""
305
+ git = get_git_integration()
306
+ if git.is_repo and git.auto_commit:
307
+ return git.auto_commit_files([file_path], tool_name=tool_name)
308
+ return None