alpiecode 7.0.0__tar.gz → 7.0.2__tar.gz

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 (44) hide show
  1. {alpiecode-7.0.0 → alpiecode-7.0.2}/PKG-INFO +1 -1
  2. {alpiecode-7.0.0 → alpiecode-7.0.2}/pyproject.toml +1 -1
  3. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/SOURCES.txt +2 -0
  5. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/agent.py +13 -0
  7. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/cli.py +21 -1
  8. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/executor.py +90 -18
  9. alpiecode-7.0.2/src/codeagent/extension/alpiecode.vsix +0 -0
  10. alpiecode-7.0.2/src/codeagent/git_ops.py +254 -0
  11. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/ipython_ext.py +76 -14
  12. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/orchestrator.py +21 -1
  13. alpiecode-7.0.2/src/codeagent/rephraser.py +137 -0
  14. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/vscode_installer.py +12 -1
  15. alpiecode-7.0.0/src/codeagent/extension/alpiecode.vsix +0 -0
  16. {alpiecode-7.0.0 → alpiecode-7.0.2}/README.md +0 -0
  17. {alpiecode-7.0.0 → alpiecode-7.0.2}/setup.cfg +0 -0
  18. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode/__init__.py +0 -0
  19. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  20. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/entry_points.txt +0 -0
  21. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/requires.txt +0 -0
  22. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/top_level.txt +0 -0
  23. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/__init__.py +0 -0
  24. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/base.py +0 -0
  25. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/local_backend.py +0 -0
  26. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/openai_backend.py +0 -0
  27. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/cache.py +0 -0
  28. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/client.py +0 -0
  29. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/compaction.py +0 -0
  30. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/config.py +0 -0
  31. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/context.py +0 -0
  32. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/discovery.py +0 -0
  33. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/doctor.py +0 -0
  34. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/github.py +0 -0
  35. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/guardian.py +0 -0
  36. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/local_model.py +0 -0
  37. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/media.py +0 -0
  38. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/memory.py +0 -0
  39. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/progress.py +0 -0
  40. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/prompt.py +0 -0
  41. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/server.py +0 -0
  42. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/session.py +0 -0
  43. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/tools.py +0 -0
  44. {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/updater.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 7.0.0
3
+ Version: 7.0.2
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "7.0.0"
7
+ version = "7.0.2"
8
8
  description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 7.0.0
3
+ Version: 7.0.2
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -18,6 +18,7 @@ src/codeagent/context.py
18
18
  src/codeagent/discovery.py
19
19
  src/codeagent/doctor.py
20
20
  src/codeagent/executor.py
21
+ src/codeagent/git_ops.py
21
22
  src/codeagent/github.py
22
23
  src/codeagent/guardian.py
23
24
  src/codeagent/ipython_ext.py
@@ -27,6 +28,7 @@ src/codeagent/memory.py
27
28
  src/codeagent/orchestrator.py
28
29
  src/codeagent/progress.py
29
30
  src/codeagent/prompt.py
31
+ src/codeagent/rephraser.py
30
32
  src/codeagent/server.py
31
33
  src/codeagent/session.py
32
34
  src/codeagent/tools.py
@@ -16,7 +16,7 @@ Supports:
16
16
  from pathlib import Path
17
17
  from typing import Optional, Any, Dict
18
18
 
19
- __version__ = "6.1.0"
19
+ __version__ = "7.0.1"
20
20
 
21
21
 
22
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "medium", **kwargs):
@@ -303,6 +303,19 @@ def run_agent(
303
303
  elif event.type == "adaptive_mode" and verbose and HAS_RICH:
304
304
  console.print("⚡ [dim]Adaptive mode: simple task detected, skipping deep reasoning for speed[/dim]")
305
305
 
306
+ elif event.type == "status":
307
+ phase = event.data.get("phase")
308
+ if debug and phase == "building" and event.data.get("rephrased") and event.data.get("rephrased") != task:
309
+ if HAS_RICH:
310
+ console.print(Panel(
311
+ f"[bold cyan]🎯 Solidified Engineering Specification[/bold cyan]\n{event.data.get('rephrased')}",
312
+ title="Internal Rephraser",
313
+ border_style="dim blue",
314
+ padding=(0, 1)
315
+ ))
316
+ else:
317
+ print(f"[Rephraser] {event.data.get('rephrased')}")
318
+
306
319
  elif event.type == "turn_start":
307
320
  current_turn = event.data["turn"]
308
321
  if debug:
@@ -29,7 +29,7 @@ def _show_banner():
29
29
 
30
30
 
31
31
  def _normalize_args():
32
- known_commands = {"init", "serve", "run", "chat", "plan", "diff", "doctor", "explain", "-h", "--help", "--version"}
32
+ known_commands = {"init", "serve", "run", "chat", "plan", "diff", "undo", "doctor", "explain", "version", "-h", "--help", "-v", "--version"}
33
33
  subcommand = None
34
34
 
35
35
  # Check if a subcommand is present
@@ -83,12 +83,15 @@ def main():
83
83
  common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
84
84
  common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
85
85
 
86
+ from . import __version__
86
87
  parser = argparse.ArgumentParser(
87
88
  prog="alpiecode",
88
89
  description="AlpieCode — Autonomous AI Coding Agent powered by 169Pi Alpie VLM",
89
90
  parents=[common],
90
91
  )
92
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
91
93
  sub = parser.add_subparsers(dest="command")
94
+ sub.add_parser("version", help="Show current version")
92
95
 
93
96
  # ── init ──
94
97
  sub.add_parser("init", help="Configure your VLM/OpenAI-compatible endpoint")
@@ -112,6 +115,9 @@ def main():
112
115
  # ── diff ──
113
116
  diff_p = sub.add_parser("diff", help="Show changes AlpieCode has made since last checkpoint", parents=[common])
114
117
 
118
+ # ── undo ──
119
+ undo_p = sub.add_parser("undo", help="Revert file changes made by the last AlpieCode session", parents=[common])
120
+
115
121
  # ── doctor ──
116
122
  sub.add_parser("doctor", help="Run system health checks (CUDA, Python, network, dev tools)")
117
123
 
@@ -125,6 +131,10 @@ def main():
125
131
  parser.print_help()
126
132
  return
127
133
 
134
+ if args.command == "version":
135
+ print(f"alpiecode {__version__}")
136
+ return
137
+
128
138
  if args.command == "init":
129
139
  interactive_init()
130
140
  return
@@ -199,6 +209,16 @@ def main():
199
209
  debug=getattr(args, "debug", False),
200
210
  )
201
211
 
212
+ elif args.command == "diff":
213
+ from .git_ops import show_diff
214
+ show_diff(Path(args.workdir).resolve())
215
+ return
216
+
217
+ elif args.command == "undo":
218
+ from .git_ops import undo_last_session
219
+ undo_last_session(Path(args.workdir).resolve())
220
+ return
221
+
202
222
  elif args.command == "doctor":
203
223
  from .doctor import run_doctor
204
224
  sys.exit(run_doctor())
@@ -116,24 +116,26 @@ class ToolExecutor:
116
116
  on_tool_start: Optional[Callable[[str, dict], None]] = None,
117
117
  on_tool_end: Optional[Callable[[str, str], None]] = None,
118
118
  ) -> List[ToolResult]:
119
- """Execute a list of tool calls using parallel dispatch for read-only tools."""
119
+ """Execute a list of tool calls with staged DAG parallelization:
120
+ - Stage 1: All read-only tools run concurrently in a ThreadPool
121
+ - Stage 2: Mutating tools run sequentially in order
122
+ Results are returned matching the exact original order of tool_calls.
123
+ """
120
124
  if not tool_calls:
121
125
  return []
122
126
 
123
- all_read_only = all(tc.name in READ_ONLY_TOOLS for tc in tool_calls)
124
- use_parallel = all_read_only and len(tool_calls) > 1
127
+ results_map: Dict[str, ToolResult] = {}
128
+ read_calls = [tc for tc in tool_calls if tc.name in READ_ONLY_TOOLS]
129
+ mut_calls = [tc for tc in tool_calls if tc.name not in READ_ONLY_TOOLS]
125
130
 
126
- results: List[ToolResult] = []
127
-
128
- if use_parallel:
129
- results_map: Dict[str, Tuple[str, float]] = {}
130
- with ThreadPoolExecutor(max_workers=min(8, len(tool_calls))) as pool:
131
+ # Stage 1: Parallel read-only execution
132
+ if len(read_calls) > 1:
133
+ with ThreadPoolExecutor(max_workers=min(8, len(read_calls))) as pool:
131
134
  future_to_tc = {}
132
- for tc in tool_calls:
135
+ for tc in read_calls:
133
136
  if on_tool_start:
134
137
  on_tool_start(tc.name, tc.arguments)
135
138
  t0 = time.monotonic()
136
- # Defensive: ensure arguments is always a dict
137
139
  safe_args = tc.arguments if isinstance(tc.arguments, dict) else {}
138
140
  fn = self.dispatch.get(tc.name)
139
141
  if fn:
@@ -149,14 +151,84 @@ class ToolExecutor:
149
151
  res_str = str(future.result())
150
152
  except Exception as e:
151
153
  res_str = f"error: {e}"
152
- results_map[tc.id] = (res_str, elapsed)
154
+ if on_tool_end:
155
+ on_tool_end(tc.name, res_str)
156
+ results_map[tc.id] = ToolResult(
157
+ tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed
158
+ )
159
+ elif len(read_calls) == 1:
160
+ tc = read_calls[0]
161
+ if on_tool_start:
162
+ on_tool_start(tc.name, tc.arguments)
163
+ t0 = time.monotonic()
164
+ safe_args = tc.arguments if isinstance(tc.arguments, dict) else {}
165
+ fn = self.dispatch.get(tc.name)
166
+ res_str = str(fn(safe_args)) if fn else f"error: Unknown tool '{tc.name}'"
167
+ elapsed = (time.monotonic() - t0) * 1000
168
+ if on_tool_end:
169
+ on_tool_end(tc.name, res_str)
170
+ results_map[tc.id] = ToolResult(
171
+ tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed
172
+ )
173
+
174
+ # Stage 2: Sequential mutating tools (write_file, edit_file, bash)
175
+ for tc in mut_calls:
176
+ if on_tool_start:
177
+ on_tool_start(tc.name, tc.arguments)
178
+ t0 = time.monotonic()
179
+ safe_args = tc.arguments if isinstance(tc.arguments, dict) else {}
180
+ fn = self.dispatch.get(tc.name)
181
+ if fn:
182
+ try:
183
+ res_str = str(fn(safe_args))
184
+ except Exception as e:
185
+ res_str = f"error: {e}"
186
+ else:
187
+ res_str = f"error: Unknown tool '{tc.name}'"
188
+ elapsed = (time.monotonic() - t0) * 1000
189
+
190
+ # Tool loop detection guard
191
+ call_sig = (tc.name, json.dumps(tc.arguments, sort_keys=True))
192
+ self.tool_call_history.append(call_sig)
193
+ repeat_count = sum(1 for item in self.tool_call_history[-5:] if item == call_sig)
194
+
195
+ if repeat_count >= 3:
196
+ res_str += (
197
+ f"\n\n🛑 REPEATED TOOL CALL LOOP DETECTED (attempt #{repeat_count}). "
198
+ f"You have already executed '{tc.name}' with these exact parameters {repeat_count} times in a row. "
199
+ "All checks have passed. Do NOT run this tool again. Output your final summary starting with: DONE: <summary>."
200
+ )
201
+
202
+ # Compilation failure recovery hint
203
+ if tc.name == "bash":
204
+ cmd = tc.arguments.get("command", "") if isinstance(tc.arguments, dict) else ""
205
+ is_compile = any(kw in cmd for kw in ["g++", "gcc", "clang", "make", "cmake", "cargo build", "rustc"])
206
+ if is_compile and "exit_code" in str(res_str):
207
+ try:
208
+ result_data = json.loads(res_str.split("\n", 1)[-1] if res_str.startswith("⚠️") else res_str)
209
+ if result_data.get("exit_code", 0) != 0:
210
+ compile_key = cmd.strip()
211
+ self.compile_fail_counts[compile_key] = self.compile_fail_counts.get(compile_key, 0) + 1
212
+ if self.compile_fail_counts[compile_key] >= 3:
213
+ res_str += (
214
+ "\n\n🛑 REPEATED COMPILATION FAILURE (attempt "
215
+ f"#{self.compile_fail_counts[compile_key]}). "
216
+ "STOP making blind edits. Re-read the ENTIRE source file with "
217
+ "read_file to understand its full structure, then fix ALL errors "
218
+ "comprehensively in one edit."
219
+ )
220
+ else:
221
+ self.compile_fail_counts.pop(cmd.strip(), None)
222
+ except (json.JSONDecodeError, ValueError):
223
+ pass
224
+
225
+ if on_tool_end:
226
+ on_tool_end(tc.name, res_str)
227
+ results_map[tc.id] = ToolResult(
228
+ tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed
229
+ )
153
230
 
154
- for tc in tool_calls:
155
- res_str, elapsed = results_map[tc.id]
156
- if on_tool_end:
157
- on_tool_end(tc.name, res_str)
158
- results.append(ToolResult(tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed))
159
- return results
231
+ return [results_map[tc.id] for tc in tool_calls if tc.id in results_map]
160
232
 
161
233
  # Sequential execution
162
234
  for tc in tool_calls:
@@ -215,4 +287,4 @@ class ToolExecutor:
215
287
 
216
288
  results.append(ToolResult(tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed))
217
289
 
218
- return results
290
+ return results
@@ -0,0 +1,254 @@
1
+ """
2
+ Git session tracking, diff inspection, and safe rollback for AlpieCode.
3
+
4
+ Features:
5
+ - Track files created / modified per session
6
+ - `show_diff()`: Render colorized, syntax-highlighted diffs of uncommitted agent changes
7
+ - `undo_last_session()`: Cleanly revert all changes made during the last agent session
8
+ """
9
+
10
+ import json
11
+ import shutil
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import List, Set, Optional, Dict, Any
15
+
16
+ from rich.console import Console
17
+ from rich.syntax import Syntax
18
+ from rich.panel import Panel
19
+
20
+ SESSION_FILE = ".alpiecode_session.json"
21
+
22
+
23
+ def record_session_start(workdir: Path) -> Dict[str, Any]:
24
+ """Capture initial git / filesystem state before agent execution begins."""
25
+ state: Dict[str, Any] = {
26
+ "is_git": False,
27
+ "initial_status": {},
28
+ "created_files": [],
29
+ "modified_files": [],
30
+ }
31
+
32
+ if shutil.which("git") and (workdir / ".git").exists():
33
+ try:
34
+ res = subprocess.run(
35
+ ["git", "status", "--porcelain"],
36
+ cwd=workdir, capture_output=True, text=True, timeout=5
37
+ )
38
+ if res.returncode == 0:
39
+ state["is_git"] = True
40
+ status_dict = {}
41
+ for line in res.stdout.splitlines():
42
+ if len(line) >= 4:
43
+ st = line[:2].strip()
44
+ f = line[3:].strip()
45
+ status_dict[f] = st
46
+ state["initial_status"] = status_dict
47
+ except Exception:
48
+ pass
49
+
50
+ return state
51
+
52
+
53
+ def record_session_end(workdir: Path, state: Dict[str, Any], touched_files: Set[str]):
54
+ """Save the session outcome to allow diffing and rolling back."""
55
+ session_data = {
56
+ "is_git": state.get("is_git", False),
57
+ "touched_files": list(touched_files),
58
+ "initial_status": state.get("initial_status", {}),
59
+ }
60
+
61
+ # Determine which files were newly created vs modified
62
+ created = []
63
+ modified = []
64
+
65
+ init_status = state.get("initial_status", {})
66
+ for f in touched_files:
67
+ full_p = workdir / f
68
+ if not full_p.exists():
69
+ continue
70
+ if f not in init_status:
71
+ # Was not tracked or did not exist before
72
+ created.append(f)
73
+ else:
74
+ modified.append(f)
75
+
76
+ session_data["created_files"] = created
77
+ session_data["modified_files"] = modified
78
+
79
+ try:
80
+ (workdir / SESSION_FILE).write_text(json.dumps(session_data, indent=2), encoding="utf-8")
81
+ except Exception:
82
+ pass
83
+
84
+
85
+ def show_diff(workdir: Path):
86
+ """Render colorized Rich diff of changes made since last session."""
87
+ console = Console()
88
+ sess_path = workdir / SESSION_FILE
89
+
90
+ # If git repo, run git diff
91
+ if shutil.which("git") and (workdir / ".git").exists():
92
+ try:
93
+ res = subprocess.run(
94
+ ["git", "diff"],
95
+ cwd=workdir, capture_output=True, text=True, timeout=8
96
+ )
97
+ diff_text = res.stdout.strip()
98
+ if diff_text:
99
+ console.print(Panel(
100
+ Syntax(diff_text, "diff", theme="monokai", line_numbers=False),
101
+ title="🔍 AlpieCode Session Diff",
102
+ border_style="cyan"
103
+ ))
104
+ return
105
+ except Exception:
106
+ pass
107
+
108
+ # Fallback to session record
109
+ if sess_path.exists():
110
+ try:
111
+ data = json.loads(sess_path.read_text(encoding="utf-8"))
112
+ created = data.get("created_files", [])
113
+ modified = data.get("modified_files", [])
114
+
115
+ if not created and not modified:
116
+ console.print("[green]✨ Clean workspace — no files modified by the last session.[/green]")
117
+ return
118
+
119
+ console.print("[bold cyan]Session File Changes:[/bold cyan]")
120
+ for c in created:
121
+ console.print(f" [green]+ created:[/green] {c}")
122
+ for m in modified:
123
+ console.print(f" [yellow]~ modified:[/yellow] {m}")
124
+ return
125
+ except Exception:
126
+ pass
127
+
128
+ console.print("[green]✨ Clean workspace — no changes found.[/green]")
129
+
130
+
131
+ def undo_last_session(workdir: Path) -> bool:
132
+ """Revert changes made during the last agent session."""
133
+ console = Console()
134
+ sess_path = workdir / SESSION_FILE
135
+
136
+ if not sess_path.exists():
137
+ # If in a git repo, check uncommitted changes
138
+ if shutil.which("git") and (workdir / ".git").exists():
139
+ try:
140
+ res = subprocess.run(
141
+ ["git", "status", "--porcelain"],
142
+ cwd=workdir, capture_output=True, text=True, timeout=5
143
+ )
144
+ if not res.stdout.strip():
145
+ console.print("[green]✨ Nothing to undo — working directory is already clean.[/green]")
146
+ return True
147
+
148
+ console.print("[yellow]Found uncommitted git changes:[/yellow]")
149
+ for line in res.stdout.splitlines()[:10]:
150
+ console.print(f" {line}")
151
+
152
+ choice = input("\nRevert all uncommitted changes? [y/N]: ").strip().lower()
153
+ if choice in ("y", "yes"):
154
+ subprocess.run(["git", "restore", "."], cwd=workdir, check=True)
155
+ subprocess.run(["git", "clean", "-fd"], cwd=workdir, check=True)
156
+ console.print("[green]✅ Successfully reverted all uncommitted changes![/green]")
157
+ return True
158
+ else:
159
+ console.print("[dim]Undo canceled.[/dim]")
160
+ return False
161
+ except Exception as e:
162
+ console.print(f"[red]Error during git undo: {e}[/red]")
163
+ return False
164
+
165
+ console.print("[yellow]No recent AlpieCode session found to undo.[/yellow]")
166
+ return False
167
+
168
+ try:
169
+ data = json.loads(sess_path.read_text(encoding="utf-8"))
170
+ created = data.get("created_files", [])
171
+ modified = data.get("modified_files", [])
172
+
173
+ if not created and not modified:
174
+ console.print("[green]✨ Nothing to undo — no files were touched in the last session.[/green]")
175
+ return True
176
+
177
+ console.print("[bold red]Files to revert:[/bold red]")
178
+ for c in created:
179
+ console.print(f" [red]- remove created:[/red] {c}")
180
+ for m in modified:
181
+ console.print(f" [yellow]↺ restore modified:[/yellow] {m}")
182
+
183
+ choice = input("\nRevert these changes? [y/N]: ").strip().lower()
184
+ if choice not in ("y", "yes"):
185
+ console.print("[dim]Undo canceled.[/dim]")
186
+ return False
187
+
188
+ # Revert modified files via git if possible
189
+ if data.get("is_git") and shutil.which("git"):
190
+ for m in modified:
191
+ try:
192
+ subprocess.run(["git", "checkout", "--", m], cwd=workdir, check=False)
193
+ except Exception:
194
+ pass
195
+
196
+ # Remove created files
197
+ for c in created:
198
+ p = workdir / c
199
+ if p.exists() and p.is_file():
200
+ try:
201
+ p.unlink()
202
+ except Exception:
203
+ pass
204
+
205
+ # Clean up session file
206
+ sess_path.unlink(missing_ok=True)
207
+ console.print(f"[green]✅ Successfully reverted {len(created) + len(modified)} files![/green]")
208
+ return True
209
+
210
+ except Exception as e:
211
+ console.print(f"[red]Failed to undo session: {e}[/red]")
212
+ return False
213
+
214
+
215
+ def push_to_remote(workdir: Path, username: Optional[str] = None, branch: Optional[str] = None) -> Dict[str, Any]:
216
+ """Stage, commit, and push changes to git remote origin."""
217
+ if not shutil.which("git") or not (workdir / ".git").exists():
218
+ return {"success": False, "error": "Not a git repository"}
219
+
220
+ try:
221
+ # Check remote
222
+ rem_res = subprocess.run(["git", "remote", "get-url", "origin"], cwd=workdir, capture_output=True, text=True, timeout=5)
223
+ if rem_res.returncode != 0:
224
+ return {"success": False, "error": "No remote 'origin' configured"}
225
+ remote_url = rem_res.stdout.strip()
226
+
227
+ # Determine branch
228
+ if not branch:
229
+ br_res = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=workdir, capture_output=True, text=True, timeout=5)
230
+ branch = br_res.stdout.strip() if br_res.returncode == 0 else "main"
231
+
232
+ # Stage and commit any uncommitted changes
233
+ subprocess.run(["git", "add", "-A"], cwd=workdir, capture_output=True, timeout=10)
234
+ commit_cmd = ["git"]
235
+ if username:
236
+ commit_cmd.extend(["-c", f"user.name={username}"])
237
+ commit_cmd.extend(["commit", "-m", "feat: updates by AlpieCode agent", "--allow-empty"])
238
+ subprocess.run(commit_cmd, cwd=workdir, capture_output=True, timeout=10)
239
+
240
+ # Push to origin
241
+ push_res = subprocess.run(["git", "push", "-u", "origin", branch], cwd=workdir, capture_output=True, text=True, timeout=30)
242
+ if push_res.returncode == 0:
243
+ return {
244
+ "success": True,
245
+ "remote": remote_url,
246
+ "branch": branch,
247
+ "username": username or "default",
248
+ "output": push_res.stdout.strip() or push_res.stderr.strip(),
249
+ }
250
+ else:
251
+ return {"success": False, "error": push_res.stderr.strip()}
252
+
253
+ except Exception as e:
254
+ return {"success": False, "error": str(e)}
@@ -11,6 +11,7 @@ and Google Colab via cell/line magic commands and rich interactive displays:
11
11
  """
12
12
 
13
13
  import html
14
+ import time
14
15
  import re
15
16
  import sys
16
17
  import warnings
@@ -30,14 +31,16 @@ from .agent import run_agent
30
31
 
31
32
 
32
33
  def _is_notebook() -> bool:
33
- """Check if code is running inside a Jupyter notebook or Google Colab."""
34
+ """Check if code is running inside a web browser notebook (Jupyter, Colab, VS Code Notebook)."""
34
35
  try:
35
36
  from IPython import get_ipython
36
37
  shell = get_ipython()
37
38
  if shell is None:
38
39
  return False
39
40
  shell_name = shell.__class__.__name__
40
- if "ZMQInteractiveShell" in shell_name or "Shell" in shell_name:
41
+ if "TerminalInteractiveShell" in shell_name:
42
+ return False
43
+ if "ZMQInteractiveShell" in shell_name or hasattr(shell, "kernel") or "google.colab" in sys.modules:
41
44
  return True
42
45
  return False
43
46
  except Exception:
@@ -112,20 +115,28 @@ def _extract_primary_code_block(text: str) -> Optional[str]:
112
115
  return None
113
116
 
114
117
 
115
- def _render_colab_card(task: str, tool_actions: List[Dict[str, str]], content: str) -> str:
116
- """Render a modern, sleek Colab/Jupyter responsive card."""
118
+ # Active session cache for multi-cell conversational memory
119
+ _ACTIVE_NOTEBOOK_SESSION = None
120
+
121
+ def _render_colab_card(task: str, tool_actions: List[Dict[str, str]], status: str = "done") -> str:
122
+ """Render a modern, sleek Colab/Jupyter responsive card with live pulse indicator."""
117
123
  task_escaped = html.escape(task[:140])
118
124
 
125
+ badge_label = "Running..." if status == "running" else "Goal-Driven"
126
+ badge_color = "#38bdf8" if status == "running" else "#64748b"
127
+
119
128
  actions_html = ""
120
129
  if tool_actions:
121
130
  actions_html = '<div style="margin-top: 8px; margin-bottom: 8px; font-family: ui-monospace, monospace; font-size: 12px;">'
122
131
  for act in tool_actions:
123
132
  name = html.escape(act.get("name", ""))
124
133
  summary = html.escape(act.get("summary", ""))
125
- status = act.get("status", "✓")
134
+ st = act.get("status", "✓")
135
+ dot_color = "#fbbf24" if st == "running" else "#38bdf8"
136
+ icon = "⏳" if st == "running" else "⏺"
126
137
  actions_html += (
127
138
  f'<div style="display: flex; align-items: center; gap: 6px; padding: 3px 0; color: #94a3b8;">'
128
- f'<span style="color: #38bdf8;">⏺</span> '
139
+ f'<span style="color: {dot_color}; font-size: 11px;">{icon}</span> '
129
140
  f'<strong style="color: #f1f5f9;">{name}</strong> '
130
141
  f'<span style="color: #64748b;">{summary}</span>'
131
142
  f'</div>'
@@ -141,8 +152,8 @@ def _render_colab_card(task: str, tool_actions: List[Dict[str, str]], content: s
141
152
  f'<span style="font-weight: 600; font-size: 13px; color: #38bdf8; display: flex; align-items: center; gap: 6px;">'
142
153
  f'⚡ AlpieCode Agent'
143
154
  f'</span>'
144
- f'<span style="font-size: 11px; color: #64748b; background: #1e293b; padding: 2px 8px; border-radius: 9999px;">'
145
- f'Goal-Driven'
155
+ f'<span style="font-size: 11px; color: {badge_color}; background: #1e293b; padding: 2px 8px; border-radius: 9999px;">'
156
+ f'{badge_label}'
146
157
  f'</span>'
147
158
  f'</div>'
148
159
  f'<div style="font-size: 13px; color: #cbd5e1; margin-top: 8px; font-weight: 500;">{task_escaped}</div>'
@@ -184,17 +195,37 @@ def alpie_magic(line: str, cell: Optional[str] = None):
184
195
  run_agent(full_task, workdir, cfg, verbose=True)
185
196
  return
186
197
 
187
- # In Jupyter or Google Colab: run with rich streaming & UI
198
+ # In Jupyter or Google Colab: run with live in-place streaming & UI
199
+ global _ACTIVE_NOTEBOOK_SESSION
188
200
  from IPython import get_ipython
201
+ from IPython.display import HTML, Markdown, display, update_display
189
202
  ip = get_ipython()
190
203
 
191
204
  backend = resolve_backend(cfg)
192
205
  orchestrator = AgentOrchestrator(backend)
193
206
  session_mgr = SessionManager()
194
- session = session_mgr.create_session(workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144)
207
+
208
+ # Support --reset or --new flag for cross-cell memory reset
209
+ if raw_task.startswith("--reset") or raw_task.startswith("--new"):
210
+ _ACTIVE_NOTEBOOK_SESSION = None
211
+ raw_task = re.sub(r"^(--reset|--new)\s*", "", raw_task).strip()
212
+ full_task = f"{raw_task}\n{nb_context}" if nb_context else raw_task
213
+
214
+ if _ACTIVE_NOTEBOOK_SESSION is None:
215
+ _ACTIVE_NOTEBOOK_SESSION = session_mgr.create_session(
216
+ workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144
217
+ )
218
+ session = _ACTIVE_NOTEBOOK_SESSION
195
219
 
196
220
  tool_actions: List[Dict[str, str]] = []
197
221
  final_content = ""
222
+ display_id = f"alpie_card_{int(time.time() * 1000)}"
223
+
224
+ # Initial in-place widget render
225
+ try:
226
+ display(HTML(_render_colab_card(raw_task, tool_actions, status="running")), display_id=display_id)
227
+ except Exception:
228
+ pass
198
229
 
199
230
  for event in orchestrator.run_task(session=session, task=full_task, cfg=cfg):
200
231
  if event.type == "tool_call":
@@ -210,10 +241,22 @@ def alpie_magic(line: str, cell: Optional[str] = None):
210
241
  summary = ""
211
242
  if t_name == "bash":
212
243
  cmd = t_args.get("command", "")
213
- summary = f"$ {cmd[:60]}..." if len(cmd) > 60 else f"$ {cmd}"
244
+ summary = f"$ {cmd[:50]}..." if len(cmd) > 50 else f"$ {cmd}"
214
245
  elif t_name in ("write_file", "edit_file", "read_file"):
215
246
  summary = str(t_args.get("path", ""))
216
- tool_actions.append({"name": t_name, "summary": summary})
247
+ tool_actions.append({"name": t_name, "summary": summary, "status": "running"})
248
+ try:
249
+ update_display(HTML(_render_colab_card(raw_task, tool_actions, status="running")), display_id=display_id)
250
+ except Exception:
251
+ pass
252
+
253
+ elif event.type == "tool_result":
254
+ if tool_actions:
255
+ tool_actions[-1]["status"] = "✓"
256
+ try:
257
+ update_display(HTML(_render_colab_card(raw_task, tool_actions, status="running")), display_id=display_id)
258
+ except Exception:
259
+ pass
217
260
 
218
261
  elif event.type == "message":
219
262
  final_content = event.data.get("content", "")
@@ -227,8 +270,12 @@ def alpie_magic(line: str, cell: Optional[str] = None):
227
270
  if cleaned_content.upper().startswith("DONE:"):
228
271
  cleaned_content = cleaned_content[5:].strip()
229
272
 
230
- # Render modern card & markdown
231
- _display_html(_render_colab_card(raw_task, tool_actions, cleaned_content))
273
+ # Final in-place update with done state
274
+ try:
275
+ update_display(HTML(_render_colab_card(raw_task, tool_actions, status="done")), display_id=display_id)
276
+ except Exception:
277
+ _display_html(_render_colab_card(raw_task, tool_actions, status="done"))
278
+
232
279
  if cleaned_content:
233
280
  _display_markdown(cleaned_content)
234
281
 
@@ -297,6 +344,20 @@ def alpie_explain_magic(line: str):
297
344
  alpie_magic(explain_task)
298
345
 
299
346
 
347
+
348
+ def alpie_reset_magic(line: str = ""):
349
+ """%alpie_reset — Reset conversation memory for the notebook session."""
350
+ global _ACTIVE_NOTEBOOK_SESSION
351
+ _ACTIVE_NOTEBOOK_SESSION = None
352
+ if _is_notebook():
353
+ _display_html(
354
+ '<div style="background: #0f172a; border-left: 3px solid #10b981; border-radius: 6px; padding: 6px 12px; font-family: sans-serif; font-size: 12px; color: #10b981;">'
355
+ '🔄 AlpieCode notebook conversation memory cleared.'
356
+ '</div>'
357
+ )
358
+ else:
359
+ print("🔄 AlpieCode notebook conversation memory cleared.")
360
+
300
361
  def alpie_doctor_magic(line: str = ""):
301
362
  """%alpie_doctor — Run system health diagnostic checks."""
302
363
  from .doctor import run_doctor
@@ -309,6 +370,7 @@ def load_ipython_extension(ipython):
309
370
  ipython.register_magic_function(alpie_plan_magic, magic_kind="line_cell", magic_name="alpie_plan")
310
371
  ipython.register_magic_function(alpie_explain_magic, magic_kind="line", magic_name="alpie_explain")
311
372
  ipython.register_magic_function(alpie_doctor_magic, magic_kind="line", magic_name="alpie_doctor")
373
+ ipython.register_magic_function(alpie_reset_magic, magic_kind="line", magic_name="alpie_reset")
312
374
  if _is_notebook():
313
375
  _display_html(
314
376
  '<div style="background: #0f172a; border-left: 4px solid #38bdf8; border-radius: 6px; padding: 8px 12px; margin: 4px 0; font-family: sans-serif; font-size: 13px; color: #e2e8f0;">'
@@ -16,6 +16,7 @@ from .config import Config, is_server_reachable
16
16
  from .memory import extract_and_save_memories
17
17
  from .discovery import build_task_context, COMPLEXITY_CONFIG
18
18
  from .progress import ProgressMonitor
19
+ from .rephraser import PromptRephraser
19
20
  from .prompt import PromptBuilder, classify_task
20
21
  from .session import Session, SessionManager
21
22
 
@@ -43,6 +44,7 @@ class AgentOrchestrator:
43
44
  ):
44
45
  self.backend = backend
45
46
  self.prompt_builder = prompt_builder or PromptBuilder()
47
+ self.rephraser = PromptRephraser()
46
48
 
47
49
  def run_task(
48
50
  self,
@@ -116,9 +118,26 @@ class AgentOrchestrator:
116
118
  task_context=task_context,
117
119
  )
118
120
  session.context.set_system_prompt(system_prompt)
121
+ # ── Phase 0.5: Internal Prompt Rephraser (first step, internal only) ──
122
+ yield AgentEvent("status", {
123
+ "phase": "rephrasing",
124
+ "message": "Solidifying task requirements...",
125
+ "task": task,
126
+ })
127
+ rephrased_task = task
128
+ try:
129
+ rephrased_task = self.rephraser.rephrase(task, self.backend, task_context)
130
+ except Exception:
131
+ rephrased_task = task
132
+
133
+ yield AgentEvent("status", {
134
+ "phase": "building",
135
+ "message": "Starting build...",
136
+ "rephrased": rephrased_task,
137
+ })
119
138
 
120
139
  user_content = self.prompt_builder.build_user_content(
121
- task=task,
140
+ task=rephrased_task,
122
141
  image_path=image_path,
123
142
  video_path=video_path,
124
143
  url=url,
@@ -134,6 +153,7 @@ class AgentOrchestrator:
134
153
  "is_offline": is_offline,
135
154
  "tool_count": len(active_tools),
136
155
  "complexity": complexity,
156
+ "rephrased_task": rephrased_task,
137
157
  })
138
158
 
139
159
 
@@ -0,0 +1,137 @@
1
+ """
2
+ Prompt rephraser service for AlpieCode.
3
+
4
+ Internally solidifies raw user prompts into high-grade software engineering
5
+ specifications before agent execution begins. Operates silently behind the scenes.
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ from typing import Any, Dict, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ REPHRASER_SYSTEM_PROMPT = """\
15
+ You are an expert AI prompt engineer and principal software architect.
16
+ Your job is to transform the user's raw prompt into a clear, solid, and unambiguous software engineering specification.
17
+
18
+ Guidelines:
19
+ 1. If the prompt is a simple Q&A, greeting, or direct conceptual question (e.g. "what is X?", "explain Y", "hi", "how does Z work"):
20
+ - Keep it clean, direct, and concise. Do NOT add unnecessary software development boilerplate.
21
+ 2. If the prompt is a coding, debugging, refactoring, testing, or feature creation task:
22
+ - Expand it into a solid, structured engineering prompt with:
23
+ * Objective: Explicit statement of what needs to be created, modified, or fixed
24
+ * Scope & Architecture: Target files, patterns, and module structure
25
+ * Technical Requirements: Key functionality, edge cases, error handling
26
+ * Quality & Implementation: Complete, production-ready code on Turn 1 with no stubs, placeholders, or missing imports
27
+ * Verification Criteria: Specific commands or tests to run to verify that the implementation works
28
+ 3. Output ONLY the refined prompt text. Do NOT include greetings, conversational remarks, thinking blocks, or preambles like "Here is the refined prompt:".
29
+ """
30
+
31
+
32
+ def _clean_rephrased_text(text: str) -> str:
33
+ """Clean model output to extract only the actual refined specification."""
34
+ if not text:
35
+ return ""
36
+
37
+ cleaned = text.strip()
38
+
39
+ # 1. Remove <think>...</think> if present
40
+ cleaned = re.sub(r"<think>.*?</think>", "", cleaned, flags=re.DOTALL).strip()
41
+
42
+ # 2. Remove "Thinking Process: ... \n\n" if model output chain-of-thought
43
+ if "thinking process:" in cleaned.lower():
44
+ # Look for double newline or start of markdown header after thinking process
45
+ match = re.search(r"(?:thinking process:.*?\n\n|\bobjective\b|\bscope\b)", cleaned, flags=re.DOTALL | re.IGNORECASE)
46
+ if match:
47
+ # If matched starting with objective/scope, slice from there
48
+ obj_match = re.search(r"(\*\*objective\*\*|objective:|#\s+objective|\*\*goal\*\*)", cleaned, flags=re.IGNORECASE)
49
+ if obj_match:
50
+ cleaned = cleaned[obj_match.start():].strip()
51
+
52
+ # 3. Clean any leading conversational fluff
53
+ fluff_prefixes = [
54
+ "here is the refined prompt:",
55
+ "here is the solidified prompt:",
56
+ "refined prompt:",
57
+ "solidified prompt:",
58
+ "here is a solid prompt:",
59
+ "here is the specification:",
60
+ "refined specification:",
61
+ ]
62
+ for prefix in fluff_prefixes:
63
+ if cleaned.lower().startswith(prefix):
64
+ cleaned = cleaned[len(prefix):].strip()
65
+ break
66
+
67
+ # 4. Strip outer code fences if accidentally wrapped
68
+ if cleaned.startswith("```") and cleaned.endswith("```"):
69
+ lines = cleaned.splitlines()
70
+ if len(lines) >= 3:
71
+ cleaned = "\n".join(lines[1:-1]).strip()
72
+
73
+ return cleaned.strip()
74
+
75
+
76
+ class PromptRephraser:
77
+ """Refines and solidifies tasks into rigorous engineering specifications."""
78
+
79
+ def __init__(self, system_prompt: str = REPHRASER_SYSTEM_PROMPT):
80
+ self.system_prompt = system_prompt
81
+
82
+ def rephrase(
83
+ self,
84
+ task: str,
85
+ backend: Any,
86
+ task_context: Optional[Any] = None,
87
+ timeout: float = 6.0,
88
+ ) -> str:
89
+ """Solidify user task into a high-grade prompt internally.
90
+
91
+ Fails open: returns original task if rephrasing fails, times out,
92
+ or if backend is unavailable.
93
+ """
94
+ if not task or not task.strip():
95
+ return task
96
+
97
+ # If pure trivial greeting, skip model call for instant response
98
+ clean_task = task.strip().lower()
99
+ if clean_task in ("hi", "hello", "hey", "help", "ping", "exit", "quit"):
100
+ return task
101
+
102
+ # Check if backend is available
103
+ if backend is None or not getattr(backend, "is_available", True):
104
+ return task
105
+
106
+ try:
107
+ messages = [
108
+ {"role": "system", "content": self.system_prompt},
109
+ {"role": "user", "content": f"User Task:\n{task.strip()}"},
110
+ ]
111
+
112
+ # Invoke backend with enable_thinking=False for ultra-fast, deterministic response
113
+ kwargs = {
114
+ "messages": messages,
115
+ "temperature": 0.0,
116
+ "max_tokens": 1024,
117
+ }
118
+ try:
119
+ response = backend.chat_completion(**kwargs, enable_thinking=False)
120
+ except TypeError:
121
+ response = backend.chat_completion(**kwargs)
122
+
123
+ refined = ""
124
+ if hasattr(response, "content") and response.content:
125
+ refined = response.content.strip()
126
+ elif isinstance(response, dict):
127
+ refined = response.get("content", "").strip()
128
+
129
+ if not refined:
130
+ return task
131
+
132
+ cleaned = _clean_rephrased_text(refined)
133
+ return cleaned if cleaned else task
134
+
135
+ except Exception as e:
136
+ logger.debug(f"Prompt rephrasing skipped due to exception: {e}")
137
+ return task
@@ -110,7 +110,18 @@ def ensure_vscode_extension(auto_confirm: bool = False, quiet: bool = False) ->
110
110
  try:
111
111
  win_path = subprocess.check_output(["wslpath", "-w", str(vsix_path)], text=True).strip()
112
112
  if win_path:
113
- target_arg = win_path
113
+ # VS Code CLI on Windows disallows UNC paths (\wsl.localhost\...). Copy to Windows temp.
114
+ if win_path.startswith("\\"):
115
+ try:
116
+ win_temp = subprocess.check_output(["cmd.exe", "/c", "echo %TEMP%"], text=True).strip()
117
+ if win_temp and not win_temp.startswith("%"):
118
+ dest = f"{win_temp}\\alpiecode.vsix"
119
+ subprocess.run(["cmd.exe", "/c", f'copy /y "{win_path}" "{dest}"'], capture_output=True)
120
+ target_arg = dest
121
+ except Exception:
122
+ target_arg = win_path
123
+ else:
124
+ target_arg = win_path
114
125
  except Exception:
115
126
  pass
116
127
 
File without changes
File without changes