openagent-framework 0.2.1__tar.gz → 0.2.3__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 (73) hide show
  1. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/PKG-INFO +1 -1
  2. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/__init__.py +1 -1
  3. openagent_framework-0.2.3/openagent/bootstrap.py +364 -0
  4. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/cli.py +130 -19
  5. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/scheduler.py +18 -13
  6. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/PKG-INFO +1 -1
  7. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/SOURCES.txt +1 -0
  8. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/pyproject.toml +1 -1
  9. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/docs/README.md +0 -0
  10. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/agent.py +0 -0
  11. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/__init__.py +0 -0
  12. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/base.py +0 -0
  13. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/discord.py +0 -0
  14. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/senders.py +0 -0
  15. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/telegram.py +0 -0
  16. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/whatsapp.py +0 -0
  17. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/config.py +0 -0
  18. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcp/__init__.py +0 -0
  19. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcp/client.py +0 -0
  20. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcp/oauth.py +0 -0
  21. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/chrome-devtools/.gitignore +0 -0
  22. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/chrome-devtools/package.json +0 -0
  23. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/.gitignore +0 -0
  24. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/package.json +0 -0
  25. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/index.ts +0 -0
  26. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/main.ts +0 -0
  27. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/tools/computer.ts +0 -0
  28. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/tools/index.ts +0 -0
  29. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/utils/response.ts +0 -0
  30. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/xdotoolStringToKeys.ts +0 -0
  31. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/tsconfig.json +0 -0
  32. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/.gitignore +0 -0
  33. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/package.json +0 -0
  34. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/src/index.ts +0 -0
  35. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/tsconfig.json +0 -0
  36. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/.gitignore +0 -0
  37. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/index.ts +0 -0
  38. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/package.json +0 -0
  39. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/tsconfig.json +0 -0
  40. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/.gitignore +0 -0
  41. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/package.json +0 -0
  42. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/src/index.ts +0 -0
  43. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/tsconfig.json +0 -0
  44. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/.gitignore +0 -0
  45. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/package.json +0 -0
  46. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/browser-pool.ts +0 -0
  47. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/content-extractor.ts +0 -0
  48. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/enhanced-content-extractor.ts +0 -0
  49. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/index.ts +0 -0
  50. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/rate-limiter.ts +0 -0
  51. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/search-engine.ts +0 -0
  52. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/types.ts +0 -0
  53. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/utils.ts +0 -0
  54. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/tsconfig.json +0 -0
  55. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/memory/__init__.py +0 -0
  56. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/memory/db.py +0 -0
  57. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/memory/manager.py +0 -0
  58. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/__init__.py +0 -0
  59. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/base.py +0 -0
  60. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/claude_api.py +0 -0
  61. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/claude_cli.py +0 -0
  62. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/zhipu.py +0 -0
  63. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/server.py +0 -0
  64. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/service.py +0 -0
  65. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/__init__.py +0 -0
  66. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/base.py +0 -0
  67. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/manager.py +0 -0
  68. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/obsidian.py +0 -0
  69. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/dependency_links.txt +0 -0
  70. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/entry_points.txt +0 -0
  71. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/requires.txt +0 -0
  72. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/top_level.txt +0 -0
  73. {openagent_framework-0.2.1 → openagent_framework-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openagent-framework
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Simplified LLM agent framework with MCP, memory, and multi-channel support
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -1,5 +1,5 @@
1
1
  from openagent.agent import Agent
2
2
  from openagent.config import load_config
3
3
 
4
- __version__ = "0.2.1"
4
+ __version__ = "0.2.3"
5
5
  __all__ = ["Agent", "load_config"]
@@ -0,0 +1,364 @@
1
+ """Platform detection, environment checks and dependency installers.
2
+
3
+ Powers `openagent doctor` and the extended `openagent setup` command.
4
+ Aim: make it as easy as possible to get OpenAgent running on a fresh
5
+ machine, across Linux / macOS / Windows, *without* pretending we can
6
+ silently install Docker Desktop on Mac/Win (we can't, legally or
7
+ practically).
8
+
9
+ Design:
10
+ - Every check returns a `Check` dataclass — name, status, message, fix hint.
11
+ - Installers either succeed, raise, or return "manual-instructions" strings.
12
+ - The caller (CLI) decides how to present the result.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import logging
19
+ import os
20
+ import platform
21
+ import shutil
22
+ import subprocess
23
+ import sys
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # ── Check result dataclass ──
31
+
32
+ Status = str # "ok" | "warn" | "fail" | "skip"
33
+
34
+
35
+ @dataclass
36
+ class Check:
37
+ name: str
38
+ status: Status
39
+ message: str
40
+ fix_hint: str = ""
41
+
42
+ @property
43
+ def ok(self) -> bool:
44
+ return self.status == "ok"
45
+
46
+
47
+ @dataclass
48
+ class Report:
49
+ checks: list[Check] = field(default_factory=list)
50
+
51
+ def add(self, chk: Check) -> None:
52
+ self.checks.append(chk)
53
+
54
+ @property
55
+ def has_failures(self) -> bool:
56
+ return any(c.status == "fail" for c in self.checks)
57
+
58
+ @property
59
+ def has_warnings(self) -> bool:
60
+ return any(c.status == "warn" for c in self.checks)
61
+
62
+
63
+ # ── Platform helpers ──
64
+
65
+ def current_platform() -> str:
66
+ system = platform.system()
67
+ if system == "Darwin":
68
+ return "macos"
69
+ if system == "Linux":
70
+ return "linux"
71
+ if system == "Windows":
72
+ return "windows"
73
+ return system.lower()
74
+
75
+
76
+ def detect_linux_pkg_manager() -> str | None:
77
+ """Return 'apt' / 'dnf' / 'pacman' / None."""
78
+ for mgr in ("apt-get", "dnf", "pacman", "zypper", "apk"):
79
+ if shutil.which(mgr):
80
+ return mgr.replace("-get", "")
81
+ return None
82
+
83
+
84
+ # ── Individual checks ──
85
+
86
+ def check_python() -> Check:
87
+ v = sys.version_info
88
+ ver = f"{v.major}.{v.minor}.{v.micro}"
89
+ if v >= (3, 11):
90
+ return Check("python", "ok", f"Python {ver}")
91
+ return Check(
92
+ "python", "fail",
93
+ f"Python {ver} — OpenAgent requires 3.11+",
94
+ "Install Python 3.11 or newer.",
95
+ )
96
+
97
+
98
+ def check_command(cmd: str, name: str | None = None) -> Check:
99
+ display = name or cmd
100
+ path = shutil.which(cmd)
101
+ if not path:
102
+ return Check(display, "warn", f"{display} not installed", f"Install {display}.")
103
+ try:
104
+ out = subprocess.run(
105
+ [cmd, "--version"], capture_output=True, text=True, timeout=5,
106
+ )
107
+ version_line = (out.stdout or out.stderr).strip().splitlines()[0] if (out.stdout or out.stderr) else "installed"
108
+ return Check(display, "ok", version_line)
109
+ except Exception:
110
+ return Check(display, "ok", f"{display} at {path}")
111
+
112
+
113
+ def check_docker() -> Check:
114
+ """Check Docker CLI + daemon reachability."""
115
+ if not shutil.which("docker"):
116
+ hint = {
117
+ "linux": "Run: openagent setup --with-docker",
118
+ "macos": "Install Docker Desktop: https://docs.docker.com/desktop/install/mac-install/",
119
+ "windows": "Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/",
120
+ }.get(current_platform(), "Install Docker.")
121
+ return Check("docker", "warn", "docker CLI not found", hint)
122
+
123
+ try:
124
+ proc = subprocess.run(
125
+ ["docker", "info", "--format", "{{.ServerVersion}}"],
126
+ capture_output=True, text=True, timeout=5,
127
+ )
128
+ if proc.returncode != 0:
129
+ hint = "Start the Docker daemon."
130
+ if current_platform() in ("macos", "windows"):
131
+ hint = "Launch Docker Desktop and wait for it to be ready."
132
+ return Check(
133
+ "docker", "warn",
134
+ "docker CLI found, but daemon is not reachable",
135
+ hint,
136
+ )
137
+ return Check("docker", "ok", f"Docker daemon {proc.stdout.strip()} reachable")
138
+ except Exception as e:
139
+ return Check("docker", "warn", f"docker check failed: {e}", "")
140
+
141
+
142
+ def check_git() -> Check:
143
+ return check_command("git")
144
+
145
+
146
+ def check_node() -> Check:
147
+ return check_command("node")
148
+
149
+
150
+ def check_openagent_config(config_path: Path) -> Check:
151
+ if not config_path.exists():
152
+ return Check(
153
+ "config",
154
+ "warn",
155
+ f"no openagent.yaml at {config_path}",
156
+ "Create one by copying an example from docs/ or passing --config.",
157
+ )
158
+ try:
159
+ import yaml
160
+ with open(config_path) as f:
161
+ yaml.safe_load(f)
162
+ return Check("config", "ok", f"{config_path}")
163
+ except Exception as e:
164
+ return Check("config", "fail", f"invalid YAML: {e}", "Fix the syntax errors.")
165
+
166
+
167
+ def check_memory_vault(config: dict) -> Check:
168
+ mem = config.get("memory", {}).get("vault_path") or "./memories"
169
+ p = Path(mem).expanduser()
170
+ if p.exists():
171
+ count = len(list(p.glob("*.md")))
172
+ return Check("memory-vault", "ok", f"{p} ({count} notes)")
173
+ return Check(
174
+ "memory-vault", "warn",
175
+ f"{p} does not exist yet",
176
+ f"Create with: mkdir -p {p}",
177
+ )
178
+
179
+
180
+ def check_services_enabled(config: dict) -> list[Check]:
181
+ """Check each enabled service has its prerequisites met."""
182
+ out: list[Check] = []
183
+ services = config.get("services", {}) or {}
184
+ for name, svc_cfg in services.items():
185
+ if not svc_cfg or not svc_cfg.get("enabled"):
186
+ continue
187
+ if name == "obsidian_web":
188
+ if not svc_cfg.get("password") and not os.environ.get("OBSIDIAN_PASSWORD"):
189
+ out.append(Check(
190
+ "service:obsidian_web", "fail",
191
+ "enabled but no password set",
192
+ "Set services.obsidian_web.password or OBSIDIAN_PASSWORD env var.",
193
+ ))
194
+ else:
195
+ out.append(Check("service:obsidian_web", "ok", "configured"))
196
+ return out
197
+
198
+
199
+ # ── Doctor: run all checks ──
200
+
201
+ def run_doctor(config: dict, config_path: Path) -> Report:
202
+ """Run the full set of checks and return a Report."""
203
+ rpt = Report()
204
+ rpt.add(check_python())
205
+ rpt.add(check_openagent_config(config_path))
206
+ rpt.add(check_memory_vault(config))
207
+ rpt.add(check_git())
208
+ rpt.add(check_node())
209
+ rpt.add(check_docker())
210
+ for c in check_services_enabled(config):
211
+ rpt.add(c)
212
+ return rpt
213
+
214
+
215
+ # ── Installers ──
216
+
217
+ def _run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
218
+ logger.info("+ %s", " ".join(cmd))
219
+ return subprocess.run(cmd, check=check)
220
+
221
+
222
+ def install_docker_linux() -> str:
223
+ """Install Docker via the distro's package manager."""
224
+ mgr = detect_linux_pkg_manager()
225
+ if mgr is None:
226
+ raise RuntimeError(
227
+ "No supported package manager found. Install Docker manually: "
228
+ "https://docs.docker.com/engine/install/"
229
+ )
230
+
231
+ sudo = ["sudo"] if os.geteuid() != 0 else []
232
+
233
+ if mgr == "apt":
234
+ env = os.environ.copy()
235
+ env["DEBIAN_FRONTEND"] = "noninteractive"
236
+ subprocess.run(sudo + ["apt-get", "update", "-q"], check=True, env=env)
237
+ subprocess.run(sudo + ["apt-get", "install", "-y", "-q", "docker.io"], check=True, env=env)
238
+ elif mgr == "dnf":
239
+ _run(sudo + ["dnf", "install", "-y", "docker"])
240
+ elif mgr == "pacman":
241
+ _run(sudo + ["pacman", "-S", "--noconfirm", "docker"])
242
+ elif mgr == "zypper":
243
+ _run(sudo + ["zypper", "--non-interactive", "install", "docker"])
244
+ elif mgr == "apk":
245
+ _run(sudo + ["apk", "add", "docker"])
246
+
247
+ # Enable + start daemon (systemd)
248
+ if shutil.which("systemctl"):
249
+ subprocess.run(sudo + ["systemctl", "enable", "--now", "docker"], check=False)
250
+
251
+ # Add current user to docker group
252
+ user = os.environ.get("SUDO_USER") or os.environ.get("USER")
253
+ if user and user != "root":
254
+ subprocess.run(sudo + ["usermod", "-aG", "docker", user], check=False)
255
+ return (
256
+ f"Docker installed. User '{user}' added to the docker group — "
257
+ "you may need to log out and back in (or run `newgrp docker`) "
258
+ "before the current shell can use docker without sudo."
259
+ )
260
+ return "Docker installed and enabled."
261
+
262
+
263
+ def install_docker_macos() -> str:
264
+ """Install Docker Desktop on macOS via Homebrew cask if available."""
265
+ if shutil.which("docker"):
266
+ return "Docker already installed."
267
+
268
+ if shutil.which("brew"):
269
+ try:
270
+ _run(["brew", "install", "--cask", "docker"])
271
+ return (
272
+ "Docker Desktop installed via Homebrew. "
273
+ "Launch the Docker app from /Applications once to accept the "
274
+ "terms of service and finish setup, then re-run "
275
+ "`openagent doctor` to verify."
276
+ )
277
+ except subprocess.CalledProcessError as e:
278
+ raise RuntimeError(f"brew install failed: {e}")
279
+
280
+ raise RuntimeError(
281
+ "Homebrew not found. Install it from https://brew.sh then re-run "
282
+ "this command, or install Docker Desktop manually from "
283
+ "https://docs.docker.com/desktop/install/mac-install/"
284
+ )
285
+
286
+
287
+ def install_docker_windows() -> str:
288
+ """Install Docker Desktop on Windows via winget if available."""
289
+ if shutil.which("docker"):
290
+ return "Docker already installed."
291
+
292
+ if shutil.which("winget"):
293
+ try:
294
+ _run([
295
+ "winget", "install", "--silent", "--accept-source-agreements",
296
+ "--accept-package-agreements", "Docker.DockerDesktop",
297
+ ])
298
+ return (
299
+ "Docker Desktop installed via winget. A reboot is usually "
300
+ "required. After rebooting, launch Docker Desktop once, then "
301
+ "re-run `openagent doctor`."
302
+ )
303
+ except subprocess.CalledProcessError as e:
304
+ raise RuntimeError(f"winget install failed: {e}")
305
+
306
+ raise RuntimeError(
307
+ "winget not found. Install Docker Desktop manually from "
308
+ "https://docs.docker.com/desktop/install/windows-install/"
309
+ )
310
+
311
+
312
+ def install_docker() -> str:
313
+ """Dispatch to the platform-specific installer."""
314
+ pf = current_platform()
315
+ if pf == "linux":
316
+ return install_docker_linux()
317
+ if pf == "macos":
318
+ return install_docker_macos()
319
+ if pf == "windows":
320
+ return install_docker_windows()
321
+ raise RuntimeError(f"Unsupported platform for automatic Docker install: {pf}")
322
+
323
+
324
+ # ── Image pulling ──
325
+
326
+ SERVICE_IMAGES = {
327
+ "obsidian_web": "lscr.io/linuxserver/obsidian:latest",
328
+ }
329
+
330
+
331
+ def enabled_service_images(config: dict) -> dict[str, str]:
332
+ out: dict[str, str] = {}
333
+ services = config.get("services", {}) or {}
334
+ for name, cfg in services.items():
335
+ if cfg and cfg.get("enabled") and name in SERVICE_IMAGES:
336
+ out[name] = SERVICE_IMAGES[name]
337
+ return out
338
+
339
+
340
+ async def pull_service_images(config: dict) -> list[tuple[str, bool, str]]:
341
+ """Pull Docker images for every enabled aux service.
342
+
343
+ Returns a list of (service_name, success, message) tuples.
344
+ """
345
+ out: list[tuple[str, bool, str]] = []
346
+ if not shutil.which("docker"):
347
+ return out
348
+
349
+ for name, image in enabled_service_images(config).items():
350
+ logger.info(f"Pulling image for {name}: {image}")
351
+ try:
352
+ proc = await asyncio.create_subprocess_exec(
353
+ "docker", "pull", image,
354
+ stdout=asyncio.subprocess.PIPE,
355
+ stderr=asyncio.subprocess.PIPE,
356
+ )
357
+ _, err = await proc.communicate()
358
+ if proc.returncode == 0:
359
+ out.append((name, True, f"pulled {image}"))
360
+ else:
361
+ out.append((name, False, f"pull failed: {err.decode(errors='replace').strip()[:200]}"))
362
+ except Exception as e:
363
+ out.append((name, False, f"pull error: {e}"))
364
+ return out
@@ -331,41 +331,152 @@ def services_cmd(ctx, action: str):
331
331
  asyncio.run(_run())
332
332
 
333
333
 
334
+ # ── Doctor: environment checks ──
335
+
336
+ _STATUS_STYLE = {
337
+ "ok": "[green]✓[/green]",
338
+ "warn": "[yellow]![/yellow]",
339
+ "fail": "[red]✗[/red]",
340
+ "skip": "[dim]·[/dim]",
341
+ }
342
+
343
+
344
+ def _print_report(report) -> None:
345
+ from rich.table import Table as _Table
346
+ tbl = _Table(show_header=True, header_style="bold")
347
+ tbl.add_column("", width=2)
348
+ tbl.add_column("Check", style="cyan")
349
+ tbl.add_column("Status")
350
+ tbl.add_column("Fix", style="dim")
351
+ for c in report.checks:
352
+ icon = _STATUS_STYLE.get(c.status, "?")
353
+ tbl.add_row(icon, c.name, c.message, c.fix_hint or "")
354
+ console.print(tbl)
355
+
356
+
357
+ @main.command("doctor")
358
+ @click.pass_context
359
+ def doctor_cmd(ctx):
360
+ """Check the environment: Python, Docker, config, enabled services."""
361
+ from pathlib import Path
362
+ from openagent.bootstrap import run_doctor, current_platform
363
+
364
+ config = ctx.obj["config"]
365
+ config_path = Path(ctx.obj["config_path"]).expanduser()
366
+
367
+ console.print(f"[bold]Platform:[/bold] {current_platform()}")
368
+ console.print()
369
+
370
+ report = run_doctor(config, config_path)
371
+ _print_report(report)
372
+
373
+ console.print()
374
+ if report.has_failures:
375
+ console.print("[red]Some checks failed.[/red] Fix the issues above and re-run `openagent doctor`.")
376
+ raise SystemExit(1)
377
+ if report.has_warnings:
378
+ console.print("[yellow]All critical checks passed, with warnings.[/yellow]")
379
+ else:
380
+ console.print("[green]All checks passed. You're good to go.[/green]")
381
+
382
+
334
383
  # ── Service management (OS-level systemd/launchd) ──
335
384
 
336
385
  @main.command("setup")
386
+ @click.option("--with-docker", is_flag=True,
387
+ help="Install Docker (Linux: via apt/dnf/pacman; Mac/Win: via brew/winget).")
388
+ @click.option("--pull-images", is_flag=True,
389
+ help="Pre-pull Docker images for every enabled aux service.")
390
+ @click.option("--full", is_flag=True,
391
+ help="Everything: doctor, install Docker if missing, pull images, register OS service.")
392
+ @click.option("--no-service", is_flag=True,
393
+ help="Skip OS service registration (systemd/launchd/Task Scheduler).")
337
394
  @click.pass_context
338
- def setup_cmd(ctx):
339
- """Detect platform, install OpenAgent as a system service, and report status."""
340
- import platform as _platform
395
+ def setup_cmd(ctx, with_docker: bool, pull_images: bool, full: bool, no_service: bool):
396
+ """First-time setup: check environment, install deps, register OS service.
397
+
398
+ By default only registers OpenAgent as an OS service. Pass --full to also
399
+ install Docker (where automatable) and pre-pull images for the services
400
+ enabled in your config.
401
+ """
402
+ from pathlib import Path
403
+ from openagent.bootstrap import (
404
+ run_doctor, install_docker, pull_service_images,
405
+ check_docker, current_platform,
406
+ )
341
407
  from openagent.service import setup_service
342
408
 
343
- console.print(f"[bold]Detected platform:[/bold] {_platform.system()}")
344
- console.print("Installing OpenAgent as a system service...")
409
+ config = ctx.obj["config"]
410
+ config_path = Path(ctx.obj["config_path"]).expanduser()
411
+
412
+ if full:
413
+ with_docker = True
414
+ pull_images = True
415
+
416
+ console.print(f"[bold]Platform:[/bold] {current_platform()}")
345
417
  console.print()
346
418
 
347
- try:
348
- info = setup_service()
349
- console.print(f"[green]{info['message']}[/green]")
350
- console.print(f"[dim]Service file:[/dim] {info['service_file']}")
419
+ # 1. Doctor pass 1
420
+ console.print("[bold]Step 1 — environment check[/bold]")
421
+ report = run_doctor(config, config_path)
422
+ _print_report(report)
423
+ console.print()
424
+
425
+ # 2. Install Docker if requested and missing
426
+ if with_docker:
427
+ console.print("[bold]Step 2 — Docker[/bold]")
428
+ docker_chk = check_docker()
429
+ if docker_chk.status == "ok":
430
+ console.print(f"[green]Docker already OK:[/green] {docker_chk.message}")
431
+ else:
432
+ try:
433
+ msg = install_docker()
434
+ console.print(f"[green]{msg}[/green]")
435
+ except Exception as e:
436
+ console.print(f"[red]Docker install failed:[/red] {e}")
351
437
  console.print()
352
- console.print("[bold]Service status:[/bold]")
353
- console.print(info["status"])
438
+
439
+ # 3. Pull images for enabled services
440
+ if pull_images:
441
+ console.print("[bold]Step 3 — pulling service images[/bold]")
442
+ results = asyncio.run(pull_service_images(config))
443
+ if not results:
444
+ console.print("[dim]No services with pullable images are enabled.[/dim]")
445
+ else:
446
+ for name, ok, msg in results:
447
+ icon = "[green]✓[/green]" if ok else "[red]✗[/red]"
448
+ console.print(f" {icon} {name}: {msg}")
354
449
  console.print()
355
- console.print(
356
- "[green]OpenAgent will now auto-start on boot "
357
- "and restart on crash.[/green]"
358
- )
359
- except Exception as e:
360
- console.print(f"[red]Setup failed: {e}[/red]")
450
+
451
+ # 4. Register OS service
452
+ if not no_service:
453
+ console.print("[bold]Step 4 — register OS service[/bold]")
454
+ try:
455
+ info = setup_service()
456
+ console.print(f"[green]{info['message']}[/green]")
457
+ console.print(f"[dim]Service file:[/dim] {info['service_file']}")
458
+ except Exception as e:
459
+ console.print(f"[red]Service registration failed:[/red] {e}")
460
+ raise SystemExit(1)
461
+ console.print()
462
+
463
+ # 5. Final re-check
464
+ console.print("[bold]Final check[/bold]")
465
+ final = run_doctor(config, config_path)
466
+ _print_report(final)
467
+ console.print()
468
+
469
+ if final.has_failures:
470
+ console.print("[red]Setup finished with failures.[/red] See above.")
361
471
  raise SystemExit(1)
472
+ console.print("[green]Setup complete.[/green]")
362
473
 
363
474
 
364
475
  @main.command("install")
365
476
  @click.pass_context
366
477
  def install_cmd(ctx):
367
- """Alias for openagent setup."""
368
- ctx.invoke(setup_cmd)
478
+ """Alias for `openagent setup --full`."""
479
+ ctx.invoke(setup_cmd, with_docker=True, pull_images=True, full=True, no_service=False)
369
480
 
370
481
 
371
482
  @main.command("uninstall")
@@ -69,24 +69,29 @@ class Scheduler:
69
69
  logger.error(f"Scheduler error: {e}")
70
70
  await asyncio.sleep(CHECK_INTERVAL)
71
71
 
72
+ async def run_task(self, task: dict) -> None:
73
+ """Execute a single task. Extension point: override or monkey-patch
74
+ this to intercept specific tasks (e.g. auto-update, which uses a
75
+ direct pip subprocess instead of going through the agent)."""
76
+ task_name = task["name"]
77
+ try:
78
+ response = await self.agent.run(
79
+ message=task["prompt"],
80
+ user_id="scheduler",
81
+ session_id=f"scheduler:{task['id']}",
82
+ )
83
+ logger.info(f"Task '{task_name}' completed: {response[:100]}...")
84
+ except Exception as e:
85
+ logger.error(f"Task '{task_name}' failed: {e}")
86
+
72
87
  async def _check_and_run(self) -> None:
73
88
  """Check for due tasks and execute them."""
74
89
  now = time.time()
75
90
  due_tasks = await self.db.get_due_tasks(now)
76
91
 
77
92
  for task in due_tasks:
78
- task_name = task["name"]
79
- logger.info(f"Running scheduled task: {task_name}")
80
-
81
- try:
82
- response = await self.agent.run(
83
- message=task["prompt"],
84
- user_id="scheduler",
85
- session_id=f"scheduler:{task['id']}",
86
- )
87
- logger.info(f"Task '{task_name}' completed: {response[:100]}...")
88
- except Exception as e:
89
- logger.error(f"Task '{task_name}' failed: {e}")
93
+ logger.info(f"Running scheduled task: {task['name']}")
94
+ await self.run_task(task)
90
95
 
91
96
  # Update last_run and compute next_run
92
97
  try:
@@ -98,7 +103,7 @@ class Scheduler:
98
103
  next_run=next_run,
99
104
  )
100
105
  except (ValueError, KeyError) as e:
101
- logger.error(f"Failed to update next_run for '{task_name}': {e}")
106
+ logger.error(f"Failed to update next_run for '{task['name']}': {e}")
102
107
 
103
108
  # ── Task management helpers ──
104
109
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openagent-framework
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Simplified LLM agent framework with MCP, memory, and multi-channel support
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -2,6 +2,7 @@ pyproject.toml
2
2
  docs/README.md
3
3
  openagent/__init__.py
4
4
  openagent/agent.py
5
+ openagent/bootstrap.py
5
6
  openagent/cli.py
6
7
  openagent/config.py
7
8
  openagent/scheduler.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openagent-framework"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "Simplified LLM agent framework with MCP, memory, and multi-channel support"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.11"