alpiecode 0.8.2__tar.gz → 0.9.1__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 (35) hide show
  1. {alpiecode-0.8.2 → alpiecode-0.9.1}/PKG-INFO +1 -1
  2. {alpiecode-0.8.2 → alpiecode-0.9.1}/pyproject.toml +5 -1
  3. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/alpiecode.egg-info/SOURCES.txt +4 -1
  5. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/agent.py +36 -14
  6. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/cli.py +5 -0
  7. alpiecode-0.9.1/src/codeagent/client.py +131 -0
  8. alpiecode-0.9.1/src/codeagent/extension/alpiecode.vsix +0 -0
  9. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/orchestrator.py +14 -3
  10. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/server.py +10 -0
  11. alpiecode-0.9.1/src/codeagent/vscode_installer.py +122 -0
  12. {alpiecode-0.8.2 → alpiecode-0.9.1}/README.md +0 -0
  13. {alpiecode-0.8.2 → alpiecode-0.9.1}/setup.cfg +0 -0
  14. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  15. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  16. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/alpiecode.egg-info/requires.txt +0 -0
  17. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  18. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/__init__.py +0 -0
  19. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/backends/__init__.py +0 -0
  20. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/backends/base.py +0 -0
  21. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/backends/local_backend.py +0 -0
  22. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/backends/openai_backend.py +0 -0
  23. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/compaction.py +0 -0
  24. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/config.py +0 -0
  25. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/context.py +0 -0
  26. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/executor.py +0 -0
  27. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/github.py +0 -0
  28. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/guardian.py +0 -0
  29. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/local_model.py +0 -0
  30. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/media.py +0 -0
  31. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/memory.py +0 -0
  32. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/prompt.py +0 -0
  33. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/session.py +0 -0
  34. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/tools.py +0 -0
  35. {alpiecode-0.8.2 → alpiecode-0.9.1}/src/codeagent/updater.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 0.8.2
3
+ Version: 0.9.1
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 = "0.8.2"
7
+ version = "0.9.1"
8
8
  description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -32,3 +32,7 @@ codeagent = "codeagent.cli:main"
32
32
 
33
33
  [tool.setuptools.packages.find]
34
34
  where = ["src"]
35
+
36
+ [tool.setuptools.package-data]
37
+ codeagent = ["extension/*.vsix"]
38
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 0.8.2
3
+ Version: 0.9.1
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
@@ -9,6 +9,7 @@ src/alpiecode.egg-info/top_level.txt
9
9
  src/codeagent/__init__.py
10
10
  src/codeagent/agent.py
11
11
  src/codeagent/cli.py
12
+ src/codeagent/client.py
12
13
  src/codeagent/compaction.py
13
14
  src/codeagent/config.py
14
15
  src/codeagent/context.py
@@ -24,7 +25,9 @@ src/codeagent/server.py
24
25
  src/codeagent/session.py
25
26
  src/codeagent/tools.py
26
27
  src/codeagent/updater.py
28
+ src/codeagent/vscode_installer.py
27
29
  src/codeagent/backends/__init__.py
28
30
  src/codeagent/backends/base.py
29
31
  src/codeagent/backends/local_backend.py
30
- src/codeagent/backends/openai_backend.py
32
+ src/codeagent/backends/openai_backend.py
33
+ src/codeagent/extension/alpiecode.vsix
@@ -149,28 +149,50 @@ def run_agent(
149
149
  video_path: str = None,
150
150
  url: str = None,
151
151
  github_repo: str = None,
152
+ server_url: str = None,
152
153
  ) -> list:
153
154
  """Run non-interactive agent task with Rich presentation."""
154
155
  workdir = workdir.resolve()
155
156
  _ensure_git(workdir)
156
157
 
157
- backend = resolve_backend(cfg)
158
- orchestrator = AgentOrchestrator(backend)
159
- session_mgr = SessionManager()
160
- session = session_mgr.create_session(workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144)
158
+ # CLI-as-Client check: auto-detect if local/remote server is running
159
+ from .client import AlpieCodeClient
160
+ target_server = server_url or getattr(cfg, "server_url", None)
161
+ if not target_server:
162
+ # Check default local server
163
+ local_client = AlpieCodeClient("http://127.0.0.1:7169")
164
+ if local_client.health().get("status") == "online":
165
+ target_server = "http://127.0.0.1:7169"
166
+
167
+ if target_server:
168
+ client = AlpieCodeClient(target_server)
169
+ event_stream = client.stream_chat(
170
+ task=task,
171
+ workdir=str(workdir),
172
+ image_path=image_path,
173
+ video_path=video_path,
174
+ url=url,
175
+ github_repo=github_repo,
176
+ )
177
+ else:
178
+ backend = resolve_backend(cfg)
179
+ orchestrator = AgentOrchestrator(backend)
180
+ session_mgr = SessionManager()
181
+ session = session_mgr.create_session(workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144)
182
+ event_stream = orchestrator.run_task(
183
+ session=session,
184
+ task=task,
185
+ cfg=cfg,
186
+ image_path=image_path,
187
+ video_path=video_path,
188
+ url=url,
189
+ github_repo=github_repo,
190
+ )
161
191
 
162
192
  _checkpoint(workdir, "checkpoint: start")
163
193
 
164
194
  current_turn = 0
165
- for event in orchestrator.run_task(
166
- session=session,
167
- task=task,
168
- cfg=cfg,
169
- image_path=image_path,
170
- video_path=video_path,
171
- url=url,
172
- github_repo=github_repo,
173
- ):
195
+ for event in event_stream:
174
196
  if event.type == "start" and verbose:
175
197
  data = event.data
176
198
  if HAS_RICH:
@@ -254,7 +276,7 @@ def run_agent(
254
276
  elif event.type == "max_turns_reached" and verbose:
255
277
  console.print(f"\n⚠️ Max turns ({event.data['max_turns']}) reached without completion.", style="bold yellow")
256
278
 
257
- return session.context.messages
279
+ return session.context.messages if "session" in locals() else []
258
280
 
259
281
 
260
282
  def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
@@ -119,6 +119,11 @@ def main():
119
119
 
120
120
  if args.command == "serve":
121
121
  _show_banner()
122
+ try:
123
+ from .vscode_installer import ensure_vscode_extension
124
+ ensure_vscode_extension()
125
+ except Exception as e:
126
+ pass
122
127
  from .server import run_server
123
128
  run_server(host=args.host, port=args.port)
124
129
  return
@@ -0,0 +1,131 @@
1
+ """
2
+ Client SDK for AlpieCode API server.
3
+
4
+ Provides a clean, typed interface for connecting to an AlpieCode server (`alpiecode serve`),
5
+ streaming SSE events, managing sessions, and canceling tasks.
6
+ Used by the CLI client mode and future IDE/remote Python integrations.
7
+ """
8
+
9
+ import json
10
+ import urllib.parse
11
+ import urllib.request
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Iterator, Optional
15
+
16
+ from .orchestrator import AgentEvent
17
+
18
+
19
+ class AlpieCodeClient:
20
+ """Client for interacting with a running AlpieCode API server."""
21
+
22
+ def __init__(self, base_url: str = "http://127.0.0.1:7169", timeout: float = 120.0):
23
+ self.base_url = base_url.rstrip("/")
24
+ self.timeout = timeout
25
+
26
+ def health(self) -> Dict[str, Any]:
27
+ """Check server health and backend status."""
28
+ req = urllib.request.Request(f"{self.base_url}/health")
29
+ try:
30
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
31
+ return json.loads(resp.read().decode("utf-8"))
32
+ except Exception as e:
33
+ return {"status": "offline", "error": str(e)}
34
+
35
+ def metrics(self) -> Dict[str, Any]:
36
+ """Get server observability metrics."""
37
+ req = urllib.request.Request(f"{self.base_url}/metrics")
38
+ try:
39
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
40
+ return json.loads(resp.read().decode("utf-8"))
41
+ except Exception as e:
42
+ return {"error": str(e)}
43
+
44
+ def list_sessions(self) -> list:
45
+ """List active server sessions."""
46
+ req = urllib.request.Request(f"{self.base_url}/sessions")
47
+ try:
48
+ with urllib.request.urlopen(req, timeout=5.0) as resp:
49
+ data = json.loads(resp.read().decode("utf-8"))
50
+ return data.get("sessions", [])
51
+ except Exception:
52
+ return []
53
+
54
+ def cancel_task(self, session_id: str) -> bool:
55
+ """Request cancellation of an in-progress task session."""
56
+ req = urllib.request.Request(
57
+ f"{self.base_url}/cancel/{session_id}",
58
+ method="POST",
59
+ headers={"Content-Type": "application/json"},
60
+ )
61
+ try:
62
+ with urllib.request.urlopen(req, timeout=5.0) as resp:
63
+ return resp.status == 200
64
+ except Exception:
65
+ return False
66
+
67
+ def stream_chat(
68
+ self,
69
+ task: str,
70
+ workdir: str = ".",
71
+ session_id: Optional[str] = None,
72
+ image_path: Optional[str] = None,
73
+ video_path: Optional[str] = None,
74
+ url: Optional[str] = None,
75
+ github_repo: Optional[str] = None,
76
+ ) -> Iterator[AgentEvent]:
77
+ """
78
+ Stream agent events from POST /chat using Server-Sent Events (SSE).
79
+ Yields AgentEvent instances.
80
+ """
81
+ endpoint = f"{self.base_url}/chat"
82
+ payload = json.dumps({
83
+ "task": task,
84
+ "workdir": str(workdir),
85
+ "session_id": session_id,
86
+ "image": image_path,
87
+ "video": video_path,
88
+ "url": url,
89
+ "github": github_repo,
90
+ }).encode("utf-8")
91
+
92
+ req = urllib.request.Request(
93
+ endpoint,
94
+ data=payload,
95
+ headers={
96
+ "Content-Type": "application/json",
97
+ "Accept": "text/event-stream",
98
+ },
99
+ )
100
+
101
+ try:
102
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
103
+ current_event_type = "message"
104
+ current_data_str = ""
105
+
106
+ for raw_line in resp:
107
+ line = raw_line.decode("utf-8").strip()
108
+ if not line:
109
+ if current_data_str:
110
+ try:
111
+ event_data = json.loads(current_data_str)
112
+ except Exception:
113
+ event_data = {"content": current_data_str}
114
+ yield AgentEvent(type=current_event_type, data=event_data)
115
+ current_event_type = "message"
116
+ current_data_str = ""
117
+ continue
118
+
119
+ if line.startswith("event:"):
120
+ current_event_type = line[6:].strip()
121
+ elif line.startswith("data:"):
122
+ current_data_str = line[5:].strip()
123
+
124
+ if current_data_str:
125
+ try:
126
+ event_data = json.loads(current_data_str)
127
+ except Exception:
128
+ event_data = {"content": current_data_str}
129
+ yield AgentEvent(type=current_event_type, data=event_data)
130
+ except Exception as e:
131
+ yield AgentEvent(type="error", data={"error": f"Server connection failed: {e}"})
@@ -28,9 +28,13 @@ class AgentEvent:
28
28
  data: Dict[str, Any]
29
29
 
30
30
 
31
- def resolve_backend(cfg: Config) -> InferenceBackend:
32
- """Resolve online vs offline backend based on server reachability."""
33
- if is_server_reachable(cfg.base_url):
31
+ def resolve_backend(cfg: Config, timeout: float = 2.0) -> InferenceBackend:
32
+ """Resolve online vs offline backend based on server reachability.
33
+
34
+ Uses a generous timeout at startup (2s default) to avoid false negatives
35
+ when the remote API is slow to respond (e.g. Azure VM cold start).
36
+ """
37
+ if is_server_reachable(cfg.base_url, timeout=timeout):
34
38
  return OpenAIBackend(cfg)
35
39
  return LocalBackend(cfg)
36
40
 
@@ -59,6 +63,13 @@ class AgentOrchestrator:
59
63
  """
60
64
  Run full agent task loop for a session. Yields AgentEvents.
61
65
  """
66
+ # Dynamic backend re-check: if currently on LocalBackend but remote
67
+ # API is now reachable, switch to OnlineBackend automatically.
68
+ # This handles the case where the server started offline but the
69
+ # remote API came online later (e.g. VM cold start, network hiccup).
70
+ if isinstance(self.backend, LocalBackend) and is_server_reachable(cfg.base_url, timeout=1.5):
71
+ self.backend = OpenAIBackend(cfg)
72
+
62
73
  is_offline = not self.backend.is_available or isinstance(self.backend, LocalBackend)
63
74
  session.is_offline = is_offline
64
75
 
@@ -13,6 +13,7 @@ from typing import Any, Dict, Optional
13
13
 
14
14
  try:
15
15
  from fastapi import FastAPI, HTTPException, Request, Response
16
+ from fastapi.middleware.cors import CORSMiddleware
16
17
  from fastapi.responses import JSONResponse, StreamingResponse
17
18
  HAS_FASTAPI = True
18
19
  except ImportError:
@@ -53,6 +54,15 @@ if HAS_FASTAPI:
53
54
  lifespan=lifespan,
54
55
  )
55
56
 
57
+ # CORS — allow VS Code extensions, web UIs, and external clients
58
+ app.add_middleware(
59
+ CORSMiddleware,
60
+ allow_origins=["*"],
61
+ allow_credentials=True,
62
+ allow_methods=["*"],
63
+ allow_headers=["*"],
64
+ )
65
+
56
66
  @app.get("/health")
57
67
  async def health_check():
58
68
  """Health check and engine status."""
@@ -0,0 +1,122 @@
1
+ """
2
+ VS Code Extension Auto-Installer for AlpieCode.
3
+
4
+ Detects if VS Code is available, checks if the AlpieCode extension is installed,
5
+ and prompts/auto-installs the bundled .vsix if missing.
6
+ """
7
+
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+
16
+ def get_bundled_vsix_path() -> Path:
17
+ """Return path to bundled .vsix inside the codeagent package."""
18
+ pkg_dir = Path(__file__).parent
19
+ return pkg_dir / "extension" / "alpiecode.vsix"
20
+
21
+
22
+ def find_code_cli() -> Optional[str]:
23
+ """Find the VS Code CLI executable across Windows, WSL, Linux, and macOS."""
24
+ for cmd in ("code", "code.cmd", "code.exe"):
25
+ found = shutil.which(cmd)
26
+ if found:
27
+ return found
28
+
29
+ # Check common WSL / Windows paths if not found directly in PATH
30
+ wsl_code_paths = [
31
+ "/mnt/c/Users/*/AppData/Local/Programs/Microsoft VS Code/bin/code",
32
+ "/usr/local/bin/code",
33
+ "/usr/bin/code",
34
+ ]
35
+ import glob
36
+ for pattern in wsl_code_paths:
37
+ matches = glob.glob(pattern)
38
+ if matches:
39
+ return matches[0]
40
+
41
+ return None
42
+
43
+
44
+ def is_extension_installed() -> bool:
45
+ """Check if 'alpiecode' extension is already installed in VS Code."""
46
+ code_bin = find_code_cli()
47
+ if not code_bin:
48
+ return False
49
+
50
+ try:
51
+ res = subprocess.run(
52
+ [code_bin, "--list-extensions"],
53
+ capture_output=True,
54
+ text=True,
55
+ timeout=8,
56
+ )
57
+ if res.returncode == 0:
58
+ installed = res.stdout.lower()
59
+ return "alpiecode" in installed
60
+ except Exception:
61
+ pass
62
+
63
+ return False
64
+
65
+
66
+ def ensure_vscode_extension(auto_confirm: bool = False, quiet: bool = False) -> bool:
67
+ """
68
+ Check if VS Code extension is installed. If missing:
69
+ - Asks user: 'Would you like to install the AlpieCode VS Code extension? [Y/n]'
70
+ - If Yes (or auto_confirm=True): installs bundled .vsix via `code --install-extension`
71
+ - Returns True if installed or successfully installed, False otherwise.
72
+ """
73
+ if is_extension_installed():
74
+ if not quiet:
75
+ print("✅ AlpieCode VS Code extension is installed.")
76
+ return True
77
+
78
+ code_bin = find_code_cli()
79
+ if not code_bin:
80
+ if not quiet:
81
+ print("ℹ️ VS Code CLI ('code') not detected on PATH. Skipping extension check.")
82
+ return False
83
+
84
+ vsix_path = get_bundled_vsix_path()
85
+ if not vsix_path.exists():
86
+ if not quiet:
87
+ print(f"ℹ️ Bundled extension VSIX not found at {vsix_path}")
88
+ return False
89
+
90
+ if not quiet:
91
+ print("\n💡 VS Code detected, but the AlpieCode extension is not installed.")
92
+
93
+ if not auto_confirm:
94
+ try:
95
+ prompt_text = " Would you like to install the AlpieCode VS Code extension now? [Y/n]: "
96
+ choice = input(prompt_text).strip().lower()
97
+ if choice and choice not in ("y", "yes"):
98
+ if not quiet:
99
+ print(" Skipped VS Code extension installation.\n")
100
+ return False
101
+ except (KeyboardInterrupt, EOFError):
102
+ print()
103
+ return False
104
+
105
+ print(f"📦 Installing AlpieCode VS Code extension ({vsix_path.name})...")
106
+ try:
107
+ res = subprocess.run(
108
+ [code_bin, "--install-extension", str(vsix_path), "--force"],
109
+ capture_output=True,
110
+ text=True,
111
+ timeout=30,
112
+ )
113
+ if res.returncode == 0:
114
+ print("✅ AlpieCode VS Code extension installed successfully!")
115
+ print(" 👉 Reload VS Code (`Ctrl+Shift+P` → `Developer: Reload Window`) to start chatting!\n")
116
+ return True
117
+ else:
118
+ print(f"⚠️ Could not install VS Code extension: {res.stderr.strip()}\n")
119
+ except Exception as e:
120
+ print(f"⚠️ Extension installation error: {e}\n")
121
+
122
+ return False
File without changes
File without changes