k-cli-for-devs 1.0.0__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 (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
k_cli/git/git_guard.py ADDED
@@ -0,0 +1,417 @@
1
+ """
2
+ git_guard.py - Git Safety Net, Automated Checkpoints & Rollback for K-CLI
3
+
4
+ Provides repository auto-initialization, shadow git checkpoints before patch application,
5
+ atomic commits on verified success, instant working-tree rollback on verification failure,
6
+ and interactive user confirmation ([Apply], [Reject], [Diff], [Auto-Fix]).
7
+ """
8
+
9
+ import os
10
+ import subprocess
11
+ import time
12
+ import uuid
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
16
+
17
+
18
+ class PatchConfirmationAction(str, Enum):
19
+ """Actions available for interactive user patch confirmation."""
20
+ APPLY = "APPLY"
21
+ REJECT = "REJECT"
22
+ DIFF = "DIFF"
23
+ AUTO_FIX = "AUTO_FIX"
24
+
25
+
26
+ class GitGuard:
27
+ """
28
+ Git safety manager providing atomic commits, diff tracking, shadow checkpoints,
29
+ automatic rollback for code modification workflows, and interactive patch review.
30
+ """
31
+
32
+ def __init__(self, repo_dir: Union[str, Path] = "."):
33
+ """
34
+ Initializes GitGuard targeting a repository workspace directory.
35
+
36
+ Args:
37
+ repo_dir: Path to the workspace / repository root directory.
38
+ """
39
+ self.repo_dir = Path(repo_dir).resolve()
40
+ self._last_snapshot: Optional[str] = None
41
+ self._last_checkpoint: Optional[str] = None
42
+ self._checkpoints: Dict[str, Dict[str, Any]] = {}
43
+
44
+ def _run_git(self, args: List[str]) -> subprocess.CompletedProcess:
45
+ """Helper to run git commands in the repository directory with default identity fallback."""
46
+ env = dict(os.environ)
47
+ env.setdefault("GIT_AUTHOR_NAME", "K-CLI")
48
+ env.setdefault("GIT_AUTHOR_EMAIL", "k-cli@local")
49
+ env.setdefault("GIT_COMMITTER_NAME", "K-CLI")
50
+ env.setdefault("GIT_COMMITTER_EMAIL", "k-cli@local")
51
+
52
+ return subprocess.run(
53
+ ["git"] + args,
54
+ cwd=str(self.repo_dir),
55
+ capture_output=True,
56
+ text=True,
57
+ env=env,
58
+ )
59
+
60
+ def is_git_repo(self) -> bool:
61
+ """
62
+ Checks whether the workspace directory is inside a valid git repository.
63
+
64
+ Returns:
65
+ True if valid git repository, False otherwise.
66
+ """
67
+ if not self.repo_dir.exists() or not self.repo_dir.is_dir():
68
+ return False
69
+
70
+ res = self._run_git(["rev-parse", "--is-inside-work-tree"])
71
+ return res.returncode == 0 and res.stdout.strip() == "true"
72
+
73
+ def ensure_repo(self) -> bool:
74
+ """
75
+ Ensures the workspace is a valid git repository, running `git init` if necessary.
76
+
77
+ Returns:
78
+ True if repository is ready, False on failure.
79
+ """
80
+ if self.is_git_repo():
81
+ return True
82
+
83
+ try:
84
+ self.repo_dir.mkdir(parents=True, exist_ok=True)
85
+ res = self._run_git(["init"])
86
+ if res.returncode != 0:
87
+ return False
88
+
89
+ # Configure local identity defaults if not already set
90
+ self._run_git(["config", "user.name", "K-CLI"])
91
+ self._run_git(["config", "user.email", "k-cli@local"])
92
+ return True
93
+ except Exception:
94
+ return False
95
+
96
+ def create_checkpoint(
97
+ self,
98
+ name: Optional[str] = None,
99
+ metadata: Optional[Dict[str, Any]] = None,
100
+ ) -> str:
101
+ """
102
+ Auto-creates a shadow git checkpoint before applying any surgical patch.
103
+ Captures HEAD SHA, working tree diffs, staged changes, and untracked files.
104
+ Registers a shadow ref under `refs/kcli/checkpoints/<id>`.
105
+
106
+ Args:
107
+ name: Optional custom checkpoint name or prefix.
108
+ metadata: Optional additional metadata dictionary to store with the checkpoint.
109
+
110
+ Returns:
111
+ Checkpoint identifier string, or empty string in non-git environment.
112
+ """
113
+ if not self.is_git_repo():
114
+ return ""
115
+
116
+ res_head = self._run_git(["rev-parse", "HEAD"])
117
+ head_sha = res_head.stdout.strip() if res_head.returncode == 0 else "EMPTY_REPO"
118
+
119
+ ts = int(time.time())
120
+ short_id = uuid.uuid4().hex[:8]
121
+ prefix = f"ckpt_{name}" if name else "ckpt"
122
+ checkpoint_id = f"{prefix}_{ts}_{short_id}"
123
+
124
+ # Capture current working tree state
125
+ res_status = self._run_git(["status", "--porcelain"])
126
+ staged_diff = self.get_diff(cached=True)
127
+ unstaged_diff = self.get_diff(cached=False)
128
+ untracked = self.get_untracked_files()
129
+
130
+ shadow_ref = None
131
+ if head_sha != "EMPTY_REPO":
132
+ shadow_ref = f"refs/kcli/checkpoints/{checkpoint_id}"
133
+ # Update shadow ref to point to current HEAD commit
134
+ self._run_git(["update-ref", shadow_ref, head_sha])
135
+
136
+ record: Dict[str, Any] = {
137
+ "checkpoint_id": checkpoint_id,
138
+ "head_sha": head_sha,
139
+ "shadow_ref": shadow_ref,
140
+ "timestamp": ts,
141
+ "status_output": res_status.stdout if res_status.returncode == 0 else "",
142
+ "staged_diff": staged_diff,
143
+ "unstaged_diff": unstaged_diff,
144
+ "untracked_files": untracked,
145
+ "metadata": metadata or {},
146
+ }
147
+
148
+ self._checkpoints[checkpoint_id] = record
149
+ self._last_checkpoint = checkpoint_id
150
+ self._last_snapshot = checkpoint_id
151
+ return checkpoint_id
152
+
153
+ def create_snapshot(self) -> str:
154
+ """
155
+ Captures a snapshot token representing the current git HEAD and workspace status.
156
+ Maintains backward compatibility while also registering a shadow checkpoint.
157
+
158
+ Returns:
159
+ Snapshot identifier string, or empty string in non-git environment.
160
+ """
161
+ if not self.is_git_repo():
162
+ return ""
163
+
164
+ res_head = self._run_git(["rev-parse", "HEAD"])
165
+ head_sha = res_head.stdout.strip() if res_head.returncode == 0 else "EMPTY_REPO"
166
+
167
+ res_status = self._run_git(["status", "--porcelain"])
168
+ status_hash = str(hash(res_status.stdout))
169
+
170
+ snapshot_id = f"snapshot_{head_sha[:10]}_{status_hash[:8]}"
171
+ self._last_snapshot = snapshot_id
172
+ # Also record checkpoint
173
+ self.create_checkpoint(name=f"snap_{head_sha[:8]}")
174
+ return snapshot_id
175
+
176
+ def restore_checkpoint(
177
+ self,
178
+ checkpoint_id: Optional[str] = None,
179
+ hard_reset: bool = True,
180
+ ) -> bool:
181
+ """
182
+ Restores workspace to the exact state of a specified shadow checkpoint.
183
+
184
+ Args:
185
+ checkpoint_id: Identifier of the checkpoint to restore. Defaults to the latest checkpoint.
186
+ hard_reset: If True, resets HEAD commit if new commits were made post-checkpoint.
187
+
188
+ Returns:
189
+ True if restoration succeeded, False otherwise.
190
+ """
191
+ if not self.is_git_repo():
192
+ return False
193
+
194
+ target_id = checkpoint_id or self._last_checkpoint
195
+ ckpt = self._checkpoints.get(target_id) if target_id else None
196
+
197
+ try:
198
+ if ckpt and ckpt.get("head_sha") and ckpt["head_sha"] != "EMPTY_REPO":
199
+ head_sha = ckpt["head_sha"]
200
+ curr_head = self._run_git(["rev-parse", "HEAD"]).stdout.strip()
201
+ if hard_reset and curr_head != head_sha:
202
+ self._run_git(["reset", "--hard", head_sha])
203
+ else:
204
+ self._run_git(["reset", "HEAD"])
205
+ self._run_git(["restore", "."])
206
+ else:
207
+ self._run_git(["reset", "HEAD"])
208
+ self._run_git(["restore", "."])
209
+
210
+ # Remove untracked files and directories created after checkpoint
211
+ self._run_git(["clean", "-fd"])
212
+ return True
213
+ except Exception:
214
+ return False
215
+
216
+ def list_checkpoints(self) -> List[str]:
217
+ """Returns list of created checkpoint IDs in chronological order."""
218
+ return list(self._checkpoints.keys())
219
+
220
+ def get_checkpoint(self, checkpoint_id: str) -> Optional[Dict[str, Any]]:
221
+ """Returns checkpoint record dictionary or None if not found."""
222
+ return self._checkpoints.get(checkpoint_id)
223
+
224
+ def delete_checkpoint(self, checkpoint_id: str) -> bool:
225
+ """Deletes a shadow checkpoint and cleans its ref."""
226
+ if checkpoint_id in self._checkpoints:
227
+ ckpt = self._checkpoints.pop(checkpoint_id)
228
+ if ckpt.get("shadow_ref"):
229
+ self._run_git(["update-ref", "-d", ckpt["shadow_ref"]])
230
+ if self._last_checkpoint == checkpoint_id:
231
+ self._last_checkpoint = list(self._checkpoints.keys())[-1] if self._checkpoints else None
232
+ return True
233
+ return False
234
+
235
+ def commit_success(
236
+ self,
237
+ message: str,
238
+ files: Optional[List[str]] = None,
239
+ ) -> Optional[str]:
240
+ """
241
+ Creates an atomic git commit with a semantic commit message on verified success.
242
+
243
+ Args:
244
+ message: Commit message (e.g. 'feat: implement quicksort').
245
+ files: Specific file paths to stage and commit. If None, stages all changes (`-A`).
246
+
247
+ Returns:
248
+ New commit SHA if committed, or current HEAD SHA if clean, or None on failure.
249
+ """
250
+ if not self.is_git_repo():
251
+ return None
252
+
253
+ # Stage files
254
+ if files:
255
+ for f in files:
256
+ add_res = self._run_git(["add", str(f)])
257
+ if add_res.returncode != 0:
258
+ # File might have been deleted, try git rm / add -u
259
+ self._run_git(["add", "-u", str(f)])
260
+ else:
261
+ self._run_git(["add", "-A"])
262
+
263
+ # Check if anything is staged
264
+ diff_cached = self._run_git(["diff", "--cached", "--quiet"])
265
+ if diff_cached.returncode == 0:
266
+ # Nothing staged to commit; return current HEAD SHA if it exists
267
+ head_res = self._run_git(["rev-parse", "HEAD"])
268
+ return head_res.stdout.strip() if head_res.returncode == 0 else None
269
+
270
+ # Commit staged changes
271
+ commit_res = self._run_git(["commit", "-m", message])
272
+ if commit_res.returncode != 0:
273
+ return None
274
+
275
+ # Retrieve and return new commit SHA
276
+ head_res = self._run_git(["rev-parse", "HEAD"])
277
+ return head_res.stdout.strip() if head_res.returncode == 0 else None
278
+
279
+ def rollback(
280
+ self,
281
+ files: Optional[List[str]] = None,
282
+ checkpoint_id: Optional[str] = None,
283
+ ) -> bool:
284
+ """
285
+ Reverts working tree modifications and unstaged changes on verification failure.
286
+ If checkpoint_id is provided, rolls back to that shadow checkpoint.
287
+
288
+ Args:
289
+ files: Specific list of files to restore. If None, restores entire working tree.
290
+ checkpoint_id: Optional checkpoint ID to restore to.
291
+
292
+ Returns:
293
+ True if rollback succeeded, False if not a git repository or on error.
294
+ """
295
+ if not self.is_git_repo():
296
+ return False
297
+
298
+ if checkpoint_id:
299
+ return self.restore_checkpoint(checkpoint_id)
300
+
301
+ try:
302
+ if files:
303
+ for f in files:
304
+ # Unstage file if staged
305
+ self._run_git(["restore", "--staged", str(f)])
306
+ # Discard modifications in working tree
307
+ self._run_git(["restore", str(f)])
308
+ # If untracked new file, clean it
309
+ self._run_git(["clean", "-f", str(f)])
310
+ else:
311
+ # Unstage all staged changes
312
+ self._run_git(["reset", "HEAD"])
313
+ # Discard modifications in working tree
314
+ self._run_git(["restore", "."])
315
+ # Remove untracked files and directories
316
+ self._run_git(["clean", "-fd"])
317
+
318
+ return True
319
+ except Exception:
320
+ return False
321
+
322
+ def get_diff(self, cached: bool = False, files: Optional[List[str]] = None) -> str:
323
+ """
324
+ Returns active git diff for the repository.
325
+
326
+ Args:
327
+ cached: If True, returns diff of staged changes (`--cached`).
328
+ If False, returns diff of unstaged working tree changes.
329
+ files: Optional specific files to limit diff to.
330
+
331
+ Returns:
332
+ Diff output string, or empty string if clean or non-git environment.
333
+ """
334
+ if not self.is_git_repo():
335
+ return ""
336
+
337
+ args = ["diff", "--cached"] if cached else ["diff"]
338
+ if files:
339
+ args.append("--")
340
+ args.extend([str(f) for f in files])
341
+
342
+ res = self._run_git(args)
343
+ return res.stdout if res.returncode == 0 else ""
344
+
345
+ def get_untracked_files(self) -> List[str]:
346
+ """Returns list of untracked files in the repository."""
347
+ if not self.is_git_repo():
348
+ return []
349
+ res = self._run_git(["ls-files", "--others", "--exclude-standard"])
350
+ if res.returncode == 0 and res.stdout.strip():
351
+ return [line.strip() for line in res.stdout.splitlines() if line.strip()]
352
+ return []
353
+
354
+ def has_uncommitted_changes(self) -> bool:
355
+ """Returns True if there are unstaged, staged, or untracked changes in the working tree."""
356
+ if not self.is_git_repo():
357
+ return False
358
+ diff = self.get_diff(cached=False)
359
+ cached_diff = self.get_diff(cached=True)
360
+ untracked = self.get_untracked_files()
361
+ return bool(diff.strip() or cached_diff.strip() or untracked)
362
+
363
+ def prompt_confirmation(
364
+ self,
365
+ diff_text: Optional[str] = None,
366
+ input_fn: Optional[Callable[[str], str]] = None,
367
+ display_fn: Optional[Callable[[str], None]] = None,
368
+ prompt_text: Optional[str] = None,
369
+ ) -> PatchConfirmationAction:
370
+ """
371
+ Interactive user confirmation prompting with [Apply], [Reject], [Diff], [Auto-Fix].
372
+
373
+ Args:
374
+ diff_text: Optional git diff string to display when [Diff] is selected.
375
+ input_fn: Callable for getting user input (defaults to builtin `input`).
376
+ display_fn: Callable for displaying messages (defaults to builtin `print`).
377
+ prompt_text: Custom prompt message.
378
+
379
+ Returns:
380
+ Selected PatchConfirmationAction (APPLY, REJECT, DIFF, or AUTO_FIX).
381
+ """
382
+ _input = input_fn or input
383
+ _display = display_fn or print
384
+ active_diff = diff_text if diff_text is not None else self.get_diff()
385
+
386
+ msg = prompt_text or "Proposed changes ready. Options: [Apply] (a), [Reject] (r), [Diff] (d), [Auto-Fix] (f)"
387
+
388
+ while True:
389
+ try:
390
+ choice = _input(f"{msg}\nSelect action: ").strip().lower()
391
+ except (EOFError, KeyboardInterrupt):
392
+ return PatchConfirmationAction.REJECT
393
+
394
+ if choice in ("apply", "a", "1", "[apply]", "y", "yes"):
395
+ return PatchConfirmationAction.APPLY
396
+ elif choice in ("reject", "r", "2", "[reject]", "n", "no", "cancel"):
397
+ return PatchConfirmationAction.REJECT
398
+ elif choice in ("diff", "d", "3", "[diff]"):
399
+ if active_diff.strip():
400
+ _display(f"\n--- Proposed Diff ---\n{active_diff}\n---------------------")
401
+ else:
402
+ _display("\n[Diff]: No changes detected in working tree.")
403
+ continue
404
+ elif choice in ("auto-fix", "autofix", "fix", "f", "4", "[auto-fix]"):
405
+ return PatchConfirmationAction.AUTO_FIX
406
+ else:
407
+ _display(f"Invalid option '{choice}'. Please choose [Apply], [Reject], [Diff], or [Auto-Fix].")
408
+
409
+
410
+ def confirm_patch_action(
411
+ diff_text: Optional[str] = None,
412
+ input_fn: Optional[Callable[[str], str]] = None,
413
+ display_fn: Optional[Callable[[str], None]] = None,
414
+ ) -> PatchConfirmationAction:
415
+ """Helper shortcut function for interactive patch confirmation."""
416
+ guard = GitGuard()
417
+ return guard.prompt_confirmation(diff_text=diff_text, input_fn=input_fn, display_fn=display_fn)