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,731 @@
1
+ """
2
+ Git Agent for NC1709
3
+ Handles Git operations: commits, branches, diffs, PRs, etc.
4
+ """
5
+ import subprocess
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Dict, Any, Optional, List
9
+ from dataclasses import dataclass
10
+
11
+ from ..base import (
12
+ Plugin, PluginMetadata, PluginCapability,
13
+ ActionResult
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class GitStatus:
19
+ """Represents git repository status"""
20
+ branch: str
21
+ ahead: int = 0
22
+ behind: int = 0
23
+ staged: List[str] = None
24
+ modified: List[str] = None
25
+ untracked: List[str] = None
26
+ conflicts: List[str] = None
27
+
28
+ def __post_init__(self):
29
+ self.staged = self.staged or []
30
+ self.modified = self.modified or []
31
+ self.untracked = self.untracked or []
32
+ self.conflicts = self.conflicts or []
33
+
34
+ @property
35
+ def is_clean(self) -> bool:
36
+ return not (self.staged or self.modified or self.untracked or self.conflicts)
37
+
38
+ @property
39
+ def has_changes(self) -> bool:
40
+ return bool(self.staged or self.modified)
41
+
42
+
43
+ @dataclass
44
+ class CommitInfo:
45
+ """Represents a git commit"""
46
+ hash: str
47
+ short_hash: str
48
+ author: str
49
+ email: str
50
+ date: str
51
+ message: str
52
+ files_changed: int = 0
53
+
54
+
55
+ class GitAgent(Plugin):
56
+ """
57
+ Git operations agent.
58
+
59
+ Provides safe, intelligent Git operations including:
60
+ - Status checking and diff viewing
61
+ - Committing with smart message generation
62
+ - Branch management
63
+ - Remote operations (push, pull, fetch)
64
+ - History viewing and searching
65
+ """
66
+
67
+ METADATA = PluginMetadata(
68
+ name="git",
69
+ version="1.0.0",
70
+ description="Git version control operations",
71
+ author="NC1709 Team",
72
+ capabilities=[
73
+ PluginCapability.VERSION_CONTROL,
74
+ PluginCapability.FILE_OPERATIONS
75
+ ],
76
+ keywords=[
77
+ "git", "commit", "push", "pull", "branch", "merge",
78
+ "diff", "status", "log", "history", "checkout", "stash",
79
+ "rebase", "cherry-pick", "remote", "fetch", "clone"
80
+ ],
81
+ config_schema={
82
+ "repo_path": {"type": "string", "default": "."},
83
+ "auto_stage": {"type": "boolean", "default": False},
84
+ "sign_commits": {"type": "boolean", "default": False},
85
+ "default_remote": {"type": "string", "default": "origin"}
86
+ }
87
+ )
88
+
89
+ @property
90
+ def metadata(self) -> PluginMetadata:
91
+ return self.METADATA
92
+
93
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
94
+ super().__init__(config)
95
+ self._repo_path: Optional[Path] = None
96
+ self._git_available = False
97
+
98
+ def initialize(self) -> bool:
99
+ """Initialize the Git agent"""
100
+ # Check if git is available
101
+ try:
102
+ result = subprocess.run(
103
+ ["git", "--version"],
104
+ capture_output=True,
105
+ text=True
106
+ )
107
+ self._git_available = result.returncode == 0
108
+ except FileNotFoundError:
109
+ self._error = "Git is not installed"
110
+ return False
111
+
112
+ # Set repository path
113
+ repo_path = self._config.get("repo_path", ".")
114
+ self._repo_path = Path(repo_path).resolve()
115
+
116
+ return True
117
+
118
+ def cleanup(self) -> None:
119
+ """Cleanup resources"""
120
+ pass
121
+
122
+ def _register_actions(self) -> None:
123
+ """Register Git actions"""
124
+ self.register_action(
125
+ "status",
126
+ self.get_status,
127
+ "Get repository status",
128
+ parameters={"detailed": {"type": "boolean", "default": False}}
129
+ )
130
+
131
+ self.register_action(
132
+ "diff",
133
+ self.get_diff,
134
+ "Show changes",
135
+ parameters={
136
+ "staged": {"type": "boolean", "default": False},
137
+ "file": {"type": "string", "optional": True}
138
+ }
139
+ )
140
+
141
+ self.register_action(
142
+ "commit",
143
+ self.commit,
144
+ "Create a commit",
145
+ parameters={
146
+ "message": {"type": "string", "required": True},
147
+ "files": {"type": "array", "optional": True},
148
+ "all": {"type": "boolean", "default": False}
149
+ },
150
+ requires_confirmation=True
151
+ )
152
+
153
+ self.register_action(
154
+ "branch",
155
+ self.manage_branch,
156
+ "Branch operations",
157
+ parameters={
158
+ "action": {"type": "string", "enum": ["list", "create", "delete", "switch"]},
159
+ "name": {"type": "string", "optional": True}
160
+ }
161
+ )
162
+
163
+ self.register_action(
164
+ "push",
165
+ self.push,
166
+ "Push to remote",
167
+ parameters={
168
+ "remote": {"type": "string", "default": "origin"},
169
+ "branch": {"type": "string", "optional": True},
170
+ "force": {"type": "boolean", "default": False}
171
+ },
172
+ requires_confirmation=True,
173
+ dangerous=True
174
+ )
175
+
176
+ self.register_action(
177
+ "pull",
178
+ self.pull,
179
+ "Pull from remote",
180
+ parameters={
181
+ "remote": {"type": "string", "default": "origin"},
182
+ "branch": {"type": "string", "optional": True},
183
+ "rebase": {"type": "boolean", "default": False}
184
+ }
185
+ )
186
+
187
+ self.register_action(
188
+ "log",
189
+ self.get_log,
190
+ "View commit history",
191
+ parameters={
192
+ "count": {"type": "integer", "default": 10},
193
+ "oneline": {"type": "boolean", "default": False},
194
+ "author": {"type": "string", "optional": True}
195
+ }
196
+ )
197
+
198
+ self.register_action(
199
+ "stash",
200
+ self.manage_stash,
201
+ "Stash operations",
202
+ parameters={
203
+ "action": {"type": "string", "enum": ["save", "pop", "list", "drop"]},
204
+ "message": {"type": "string", "optional": True}
205
+ }
206
+ )
207
+
208
+ self.register_action(
209
+ "reset",
210
+ self.reset,
211
+ "Reset changes",
212
+ parameters={
213
+ "mode": {"type": "string", "enum": ["soft", "mixed", "hard"], "default": "mixed"},
214
+ "target": {"type": "string", "default": "HEAD"}
215
+ },
216
+ requires_confirmation=True,
217
+ dangerous=True
218
+ )
219
+
220
+ def _run_git(self, *args, cwd: Optional[Path] = None) -> subprocess.CompletedProcess:
221
+ """Run a git command
222
+
223
+ Args:
224
+ *args: Git command arguments
225
+ cwd: Working directory
226
+
227
+ Returns:
228
+ CompletedProcess result
229
+ """
230
+ cmd = ["git"] + list(args)
231
+ return subprocess.run(
232
+ cmd,
233
+ cwd=cwd or self._repo_path,
234
+ capture_output=True,
235
+ text=True
236
+ )
237
+
238
+ def is_git_repo(self, path: Optional[Path] = None) -> bool:
239
+ """Check if path is a git repository"""
240
+ result = self._run_git("rev-parse", "--git-dir", cwd=path)
241
+ return result.returncode == 0
242
+
243
+ def get_status(self, detailed: bool = False) -> ActionResult:
244
+ """Get repository status
245
+
246
+ Args:
247
+ detailed: Include detailed file information
248
+
249
+ Returns:
250
+ ActionResult with GitStatus
251
+ """
252
+ if not self.is_git_repo():
253
+ return ActionResult.fail("Not a git repository")
254
+
255
+ # Get branch name
256
+ result = self._run_git("branch", "--show-current")
257
+ branch = result.stdout.strip() or "HEAD"
258
+
259
+ # Get status
260
+ result = self._run_git("status", "--porcelain", "-b")
261
+ if result.returncode != 0:
262
+ return ActionResult.fail(result.stderr)
263
+
264
+ lines = result.stdout.strip().split("\n")
265
+
266
+ status = GitStatus(branch=branch)
267
+
268
+ # Parse ahead/behind from first line
269
+ if lines and lines[0].startswith("##"):
270
+ branch_line = lines[0]
271
+ ahead_match = re.search(r"ahead (\d+)", branch_line)
272
+ behind_match = re.search(r"behind (\d+)", branch_line)
273
+ if ahead_match:
274
+ status.ahead = int(ahead_match.group(1))
275
+ if behind_match:
276
+ status.behind = int(behind_match.group(1))
277
+ lines = lines[1:]
278
+
279
+ # Parse file statuses
280
+ for line in lines:
281
+ if not line:
282
+ continue
283
+
284
+ index_status = line[0]
285
+ worktree_status = line[1]
286
+ filename = line[3:]
287
+
288
+ if index_status == "U" or worktree_status == "U":
289
+ status.conflicts.append(filename)
290
+ elif index_status != " " and index_status != "?":
291
+ status.staged.append(filename)
292
+
293
+ if worktree_status == "M":
294
+ status.modified.append(filename)
295
+ elif worktree_status == "?":
296
+ status.untracked.append(filename)
297
+
298
+ # Build message
299
+ msg_parts = [f"On branch {status.branch}"]
300
+
301
+ if status.ahead:
302
+ msg_parts.append(f"ahead by {status.ahead} commit(s)")
303
+ if status.behind:
304
+ msg_parts.append(f"behind by {status.behind} commit(s)")
305
+
306
+ if status.is_clean:
307
+ msg_parts.append("Working tree clean")
308
+ else:
309
+ if status.staged:
310
+ msg_parts.append(f"{len(status.staged)} staged")
311
+ if status.modified:
312
+ msg_parts.append(f"{len(status.modified)} modified")
313
+ if status.untracked:
314
+ msg_parts.append(f"{len(status.untracked)} untracked")
315
+ if status.conflicts:
316
+ msg_parts.append(f"{len(status.conflicts)} conflicts")
317
+
318
+ return ActionResult.ok(
319
+ message=", ".join(msg_parts),
320
+ data=status
321
+ )
322
+
323
+ def get_diff(
324
+ self,
325
+ staged: bool = False,
326
+ file: Optional[str] = None
327
+ ) -> ActionResult:
328
+ """Get diff of changes
329
+
330
+ Args:
331
+ staged: Show staged changes only
332
+ file: Specific file to diff
333
+
334
+ Returns:
335
+ ActionResult with diff content
336
+ """
337
+ args = ["diff"]
338
+
339
+ if staged:
340
+ args.append("--staged")
341
+
342
+ if file:
343
+ args.append("--")
344
+ args.append(file)
345
+
346
+ result = self._run_git(*args)
347
+
348
+ if result.returncode != 0:
349
+ return ActionResult.fail(result.stderr)
350
+
351
+ diff = result.stdout
352
+
353
+ if not diff:
354
+ return ActionResult.ok("No changes", data="")
355
+
356
+ # Count changes
357
+ additions = diff.count("\n+") - diff.count("\n+++")
358
+ deletions = diff.count("\n-") - diff.count("\n---")
359
+
360
+ return ActionResult.ok(
361
+ message=f"+{additions} -{deletions} lines",
362
+ data=diff
363
+ )
364
+
365
+ def commit(
366
+ self,
367
+ message: str,
368
+ files: Optional[List[str]] = None,
369
+ all: bool = False
370
+ ) -> ActionResult:
371
+ """Create a commit
372
+
373
+ Args:
374
+ message: Commit message
375
+ files: Specific files to commit
376
+ all: Stage all changes (-a flag)
377
+
378
+ Returns:
379
+ ActionResult with commit info
380
+ """
381
+ if not message:
382
+ return ActionResult.fail("Commit message required")
383
+
384
+ # Stage files if specified
385
+ if files:
386
+ result = self._run_git("add", *files)
387
+ if result.returncode != 0:
388
+ return ActionResult.fail(f"Failed to stage files: {result.stderr}")
389
+
390
+ # Build commit command
391
+ args = ["commit"]
392
+
393
+ if all:
394
+ args.append("-a")
395
+
396
+ if self._config.get("sign_commits"):
397
+ args.append("-S")
398
+
399
+ args.extend(["-m", message])
400
+
401
+ result = self._run_git(*args)
402
+
403
+ if result.returncode != 0:
404
+ if "nothing to commit" in result.stdout:
405
+ return ActionResult.ok("Nothing to commit", data=None)
406
+ return ActionResult.fail(result.stderr or result.stdout)
407
+
408
+ # Get commit hash
409
+ hash_result = self._run_git("rev-parse", "--short", "HEAD")
410
+ commit_hash = hash_result.stdout.strip()
411
+
412
+ return ActionResult.ok(
413
+ message=f"Created commit {commit_hash}",
414
+ data={"hash": commit_hash, "message": message}
415
+ )
416
+
417
+ def manage_branch(
418
+ self,
419
+ action: str = "list",
420
+ name: Optional[str] = None
421
+ ) -> ActionResult:
422
+ """Branch management operations
423
+
424
+ Args:
425
+ action: Operation (list, create, delete, switch)
426
+ name: Branch name for create/delete/switch
427
+
428
+ Returns:
429
+ ActionResult
430
+ """
431
+ if action == "list":
432
+ result = self._run_git("branch", "-a", "-v")
433
+ if result.returncode != 0:
434
+ return ActionResult.fail(result.stderr)
435
+
436
+ branches = []
437
+ current = None
438
+ for line in result.stdout.strip().split("\n"):
439
+ if line.startswith("*"):
440
+ current = line[2:].split()[0]
441
+ branches.append(line[2:].strip())
442
+ else:
443
+ branches.append(line.strip())
444
+
445
+ return ActionResult.ok(
446
+ message=f"Current: {current}, {len(branches)} branches",
447
+ data={"current": current, "branches": branches}
448
+ )
449
+
450
+ elif action == "create":
451
+ if not name:
452
+ return ActionResult.fail("Branch name required")
453
+
454
+ result = self._run_git("checkout", "-b", name)
455
+ if result.returncode != 0:
456
+ return ActionResult.fail(result.stderr)
457
+
458
+ return ActionResult.ok(f"Created and switched to branch '{name}'")
459
+
460
+ elif action == "delete":
461
+ if not name:
462
+ return ActionResult.fail("Branch name required")
463
+
464
+ result = self._run_git("branch", "-d", name)
465
+ if result.returncode != 0:
466
+ return ActionResult.fail(result.stderr)
467
+
468
+ return ActionResult.ok(f"Deleted branch '{name}'")
469
+
470
+ elif action == "switch":
471
+ if not name:
472
+ return ActionResult.fail("Branch name required")
473
+
474
+ result = self._run_git("checkout", name)
475
+ if result.returncode != 0:
476
+ return ActionResult.fail(result.stderr)
477
+
478
+ return ActionResult.ok(f"Switched to branch '{name}'")
479
+
480
+ return ActionResult.fail(f"Unknown action: {action}")
481
+
482
+ def push(
483
+ self,
484
+ remote: str = "origin",
485
+ branch: Optional[str] = None,
486
+ force: bool = False
487
+ ) -> ActionResult:
488
+ """Push to remote
489
+
490
+ Args:
491
+ remote: Remote name
492
+ branch: Branch to push
493
+ force: Force push
494
+
495
+ Returns:
496
+ ActionResult
497
+ """
498
+ args = ["push", remote]
499
+
500
+ if branch:
501
+ args.append(branch)
502
+
503
+ if force:
504
+ args.append("--force")
505
+
506
+ result = self._run_git(*args)
507
+
508
+ if result.returncode != 0:
509
+ return ActionResult.fail(result.stderr)
510
+
511
+ return ActionResult.ok(
512
+ message=f"Pushed to {remote}" + (f"/{branch}" if branch else ""),
513
+ data=result.stdout
514
+ )
515
+
516
+ def pull(
517
+ self,
518
+ remote: str = "origin",
519
+ branch: Optional[str] = None,
520
+ rebase: bool = False
521
+ ) -> ActionResult:
522
+ """Pull from remote
523
+
524
+ Args:
525
+ remote: Remote name
526
+ branch: Branch to pull
527
+ rebase: Use rebase instead of merge
528
+
529
+ Returns:
530
+ ActionResult
531
+ """
532
+ args = ["pull"]
533
+
534
+ if rebase:
535
+ args.append("--rebase")
536
+
537
+ args.append(remote)
538
+
539
+ if branch:
540
+ args.append(branch)
541
+
542
+ result = self._run_git(*args)
543
+
544
+ if result.returncode != 0:
545
+ return ActionResult.fail(result.stderr)
546
+
547
+ return ActionResult.ok(
548
+ message=f"Pulled from {remote}" + (f"/{branch}" if branch else ""),
549
+ data=result.stdout
550
+ )
551
+
552
+ def get_log(
553
+ self,
554
+ count: int = 10,
555
+ oneline: bool = False,
556
+ author: Optional[str] = None
557
+ ) -> ActionResult:
558
+ """Get commit history
559
+
560
+ Args:
561
+ count: Number of commits
562
+ oneline: One line per commit
563
+ author: Filter by author
564
+
565
+ Returns:
566
+ ActionResult with commits
567
+ """
568
+ args = ["log", f"-{count}"]
569
+
570
+ if oneline:
571
+ args.append("--oneline")
572
+ else:
573
+ args.append("--format=%H|%h|%an|%ae|%ad|%s")
574
+ args.append("--date=short")
575
+
576
+ if author:
577
+ args.append(f"--author={author}")
578
+
579
+ result = self._run_git(*args)
580
+
581
+ if result.returncode != 0:
582
+ return ActionResult.fail(result.stderr)
583
+
584
+ if oneline:
585
+ return ActionResult.ok(
586
+ message=f"Last {count} commits",
587
+ data=result.stdout.strip()
588
+ )
589
+
590
+ # Parse formatted output
591
+ commits = []
592
+ for line in result.stdout.strip().split("\n"):
593
+ if not line:
594
+ continue
595
+ parts = line.split("|")
596
+ if len(parts) >= 6:
597
+ commits.append(CommitInfo(
598
+ hash=parts[0],
599
+ short_hash=parts[1],
600
+ author=parts[2],
601
+ email=parts[3],
602
+ date=parts[4],
603
+ message=parts[5]
604
+ ))
605
+
606
+ return ActionResult.ok(
607
+ message=f"Last {len(commits)} commits",
608
+ data=commits
609
+ )
610
+
611
+ def manage_stash(
612
+ self,
613
+ action: str = "list",
614
+ message: Optional[str] = None
615
+ ) -> ActionResult:
616
+ """Stash operations
617
+
618
+ Args:
619
+ action: Operation (save, pop, list, drop)
620
+ message: Stash message for save
621
+
622
+ Returns:
623
+ ActionResult
624
+ """
625
+ if action == "list":
626
+ result = self._run_git("stash", "list")
627
+ return ActionResult.ok(
628
+ message=f"{len(result.stdout.strip().split(chr(10)))} stashes" if result.stdout.strip() else "No stashes",
629
+ data=result.stdout.strip()
630
+ )
631
+
632
+ elif action == "save":
633
+ args = ["stash", "push"]
634
+ if message:
635
+ args.extend(["-m", message])
636
+
637
+ result = self._run_git(*args)
638
+ if result.returncode != 0:
639
+ return ActionResult.fail(result.stderr)
640
+
641
+ return ActionResult.ok("Changes stashed", data=result.stdout)
642
+
643
+ elif action == "pop":
644
+ result = self._run_git("stash", "pop")
645
+ if result.returncode != 0:
646
+ return ActionResult.fail(result.stderr)
647
+
648
+ return ActionResult.ok("Stash applied and dropped", data=result.stdout)
649
+
650
+ elif action == "drop":
651
+ result = self._run_git("stash", "drop")
652
+ if result.returncode != 0:
653
+ return ActionResult.fail(result.stderr)
654
+
655
+ return ActionResult.ok("Stash dropped", data=result.stdout)
656
+
657
+ return ActionResult.fail(f"Unknown action: {action}")
658
+
659
+ def reset(
660
+ self,
661
+ mode: str = "mixed",
662
+ target: str = "HEAD"
663
+ ) -> ActionResult:
664
+ """Reset changes
665
+
666
+ Args:
667
+ mode: Reset mode (soft, mixed, hard)
668
+ target: Reset target
669
+
670
+ Returns:
671
+ ActionResult
672
+ """
673
+ args = ["reset", f"--{mode}", target]
674
+
675
+ result = self._run_git(*args)
676
+
677
+ if result.returncode != 0:
678
+ return ActionResult.fail(result.stderr)
679
+
680
+ return ActionResult.ok(
681
+ message=f"Reset ({mode}) to {target}",
682
+ data=result.stdout
683
+ )
684
+
685
+ def can_handle(self, request: str) -> float:
686
+ """Check if request is git-related"""
687
+ request_lower = request.lower()
688
+
689
+ # High confidence keywords
690
+ high_conf = ["git ", "commit", "push", "pull", "branch", "merge", "diff"]
691
+ for kw in high_conf:
692
+ if kw in request_lower:
693
+ return 0.9
694
+
695
+ # Medium confidence
696
+ med_conf = ["changes", "history", "checkout", "stash", "log"]
697
+ for kw in med_conf:
698
+ if kw in request_lower:
699
+ return 0.6
700
+
701
+ return super().can_handle(request)
702
+
703
+ def handle_request(self, request: str, **kwargs) -> Optional[ActionResult]:
704
+ """Handle a natural language request
705
+
706
+ Args:
707
+ request: User's request
708
+
709
+ Returns:
710
+ ActionResult or None
711
+ """
712
+ request_lower = request.lower()
713
+
714
+ # Status
715
+ if any(kw in request_lower for kw in ["status", "what changed", "changes"]):
716
+ return self.get_status(detailed=True)
717
+
718
+ # Diff
719
+ if "diff" in request_lower or "show changes" in request_lower:
720
+ staged = "staged" in request_lower
721
+ return self.get_diff(staged=staged)
722
+
723
+ # Log
724
+ if any(kw in request_lower for kw in ["log", "history", "commits"]):
725
+ return self.get_log(count=10, oneline=True)
726
+
727
+ # Branch list
728
+ if "branch" in request_lower and "list" in request_lower:
729
+ return self.manage_branch(action="list")
730
+
731
+ return None