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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- 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
|