aicortexpy 0.1.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 (58) hide show
  1. aicortexpy/__init__.py +3 -0
  2. aicortexpy/__main__.py +5 -0
  3. aicortexpy/acp_client.py +559 -0
  4. aicortexpy/acp_conn.py +853 -0
  5. aicortexpy/acp_errors.py +107 -0
  6. aicortexpy/agent_env.py +88 -0
  7. aicortexpy/artifact_ws.py +233 -0
  8. aicortexpy/artifacts.py +67 -0
  9. aicortexpy/attachments.py +85 -0
  10. aicortexpy/auto_commit.py +106 -0
  11. aicortexpy/capabilities.py +92 -0
  12. aicortexpy/chat_acp_pool.py +321 -0
  13. aicortexpy/cli.py +133 -0
  14. aicortexpy/client.py +438 -0
  15. aicortexpy/codex_config.py +98 -0
  16. aicortexpy/config.py +298 -0
  17. aicortexpy/daemon.py +677 -0
  18. aicortexpy/data/default_skills/agent-memory/SKILL.md +67 -0
  19. aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +144 -0
  20. aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +145 -0
  21. aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +87 -0
  22. aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +96 -0
  23. aicortexpy/data/default_skills/aicortex-basics/SKILL.md +10 -0
  24. aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +118 -0
  25. aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +100 -0
  26. aicortexpy/data/default_skills/aicortex-graph/SKILL.md +30 -0
  27. aicortexpy/data/default_skills/graphify/SKILL.md +97 -0
  28. aicortexpy/data/default_skills/question-form/SKILL.md +162 -0
  29. aicortexpy/data/default_skills/test-report-generator/SKILL.md +179 -0
  30. aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +146 -0
  31. aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +275 -0
  32. aicortexpy/design_seed.py +59 -0
  33. aicortexpy/diskusage.py +209 -0
  34. aicortexpy/execenv.py +264 -0
  35. aicortexpy/gc.py +286 -0
  36. aicortexpy/handoff_bundle.py +65 -0
  37. aicortexpy/health.py +178 -0
  38. aicortexpy/local_skills.py +157 -0
  39. aicortexpy/models.py +385 -0
  40. aicortexpy/openclaw_config.py +153 -0
  41. aicortexpy/pinned_workdir.py +60 -0
  42. aicortexpy/platform_info.py +61 -0
  43. aicortexpy/poisoned.py +44 -0
  44. aicortexpy/process.py +168 -0
  45. aicortexpy/procutil.py +61 -0
  46. aicortexpy/prompt.py +235 -0
  47. aicortexpy/repo.py +462 -0
  48. aicortexpy/runner.py +969 -0
  49. aicortexpy/runtime_config.py +540 -0
  50. aicortexpy/sandbox.py +264 -0
  51. aicortexpy/stream.py +422 -0
  52. aicortexpy/terminal.py +147 -0
  53. aicortexpy/tunnel.py +82 -0
  54. aicortexpy/wakeup.py +222 -0
  55. aicortexpy-0.1.0.dist-info/METADATA +106 -0
  56. aicortexpy-0.1.0.dist-info/RECORD +58 -0
  57. aicortexpy-0.1.0.dist-info/WHEEL +5 -0
  58. aicortexpy-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,92 @@
1
+ """Go daemon capability inventory vs Python daemon status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Capability:
10
+ id: str
11
+ name: str
12
+ go_ref: str
13
+ status: str
14
+ python_note: str
15
+ sandbox_note: str = ""
16
+
17
+
18
+ CAPABILITIES: list[Capability] = [
19
+ Capability("register", "Register runtimes per workspace", "daemon.go", "done", "OK"),
20
+ Capability("deregister", "Deregister on shutdown", "daemon.go", "done", "OK"),
21
+ Capability("workspace_sync", "Periodic workspace discovery", "workspaceSyncLoop", "done", "30s sync + re-register empty"),
22
+ Capability("runtime_gone", "Recover when server deletes runtime", "handleRuntimeGone", "done", "404 heartbeat → backoff re-register"),
23
+ Capability("identity", "Stable daemon_id / device_name / X-Client-*", "identity.go", "done", "OK"),
24
+ Capability("health", "Local health + shutdown + /repo/checkout", "health.go", "done", "19514 (+ profile offset); allowlist checkout"),
25
+ Capability("background", "Detach start/stop/status", "cmd_daemon_*", "done", "Win/macOS/Linux; --backend python"),
26
+ Capability("poll_claim", "Poll claim tasks", "pollLoop", "done", "OK"),
27
+ Capability("wakeup_ws", "WebSocket wakeup + sidebands", "wakeup.go", "done", "task + tunnel + artifact + terminal mux"),
28
+ Capability("start_complete_fail", "start / progress / complete / fail", "client.go", "done", "OK"),
29
+ Capability("cancel_watch", "Cancel mid-run via status poll", "watchTaskCancellation", "done", "kill process tree"),
30
+ Capability("messages_usage", "Report stream messages + token usage", "ReportTaskMessages/Usage", "done", "claude/cursor/gemini/opencode/copilot + ACP"),
31
+ Capability("prompt_build", "Rich prompt templates", "prompt.go", "done", "templates + reply_instructions"),
32
+ Capability("recover_orphans", "Recover orphans on register", "RecoverOrphans", "done", "OK"),
33
+ Capability("execenv_prepare", "Per-task env + skills", "execenv.Prepare", "done", "prior reuse + agentEnv + handoff/seed"),
34
+ Capability("runtime_meta", "CLAUDE.md/AGENTS.md/GEMINI.md meta skill", "runtime_config.go", "done", "runtime_config.py"),
35
+ Capability("codex_home", "Per-task CODEX_HOME", "codex_home.go", "done", "hydrate + skill strip + multi_agent off"),
36
+ Capability("openclaw_config", "Per-task OpenClaw config wrap", "openclaw_config.go", "done", "fail-closed + full agent entry rewrite"),
37
+ Capability("repo_cache", "Bare cache + worktree checkout", "repocache/", "done", "per-ws bare + lock + worktree + co-authored hook"),
38
+ Capability("pinned_workdir", "Pinned project workdir", "pinned_workdir.go", "done", "projects/<id> stages"),
39
+ Capability("attachments", "Materialize attachments", "materialize_attachments.go", "done", "_incoming/"),
40
+ Capability("auto_commit", "Auto-commit agent changes", "auto_commit.go", "done", "best-effort commit/push"),
41
+ Capability("artifacts", "Artifacts metadata + WS browse/chunk", "artifact/", "done", "scan metadata + wakeup artifact ops"),
42
+ Capability("agent_claude", "Claude stream-json backend", "claude.go", "done", "stream-json + resume + mcp-config"),
43
+ Capability("agent_codex", "Codex exec backend", "codex.go", "done", "codex exec + CODEX_HOME + semantic inactivity"),
44
+ Capability("agent_others", "Other provider CLIs", "pkg/agent", "done", "openclaw/pi/opencode/copilot/cursor/gemini"),
45
+ Capability(
46
+ "chat_acp_pool",
47
+ "ACP connection pool",
48
+ "chat_acp_pool.go",
49
+ "done",
50
+ "Cursor/Kiro chat pool; kiro/hermes/kimi/dsh oneshot ACP",
51
+ ),
52
+ Capability(
53
+ "agent_acp_oneshot",
54
+ "ACP one-shot backends",
55
+ "kiro.go/hermes.go/kimi.go/dsh.go",
56
+ "done",
57
+ "JSON-RPC + tool stream + stderr promote + idle watchdog",
58
+ ),
59
+ Capability("model_list", "Heartbeat model discovery", "handleModelList", "done", "CLI + ACP discovery + 60s cache"),
60
+ Capability("local_skills", "List/import local skills", "local_skills.go", "done", "local_skills.py"),
61
+ Capability("default_skills", "Bundled default skills", "defaultskills/", "done", "bundled + monorepo sync"),
62
+ Capability("heartbeat", "Runtime heartbeat + actions", "heartbeatLoop", "done", "model/skills; skip update"),
63
+ Capability("auto_update", "Self-update CLI binary", "auto_update.go", "n/a", "use pip"),
64
+ Capability("gc", "Workspace GC / diskusage", "gc.go", "done", ".gc_meta.json + active-root guard"),
65
+ Capability("poisoned", "Poisoned-task quarantine", "poisoned.go", "done", "failure_reason classifiers"),
66
+ Capability("terminal_pty", "Remote terminal", "terminal_*.go", "partial", "pipe-shell MVP; aicortex_TERMINAL_PTY"),
67
+ Capability("tunnel_proxy", "Loopback HTTP tunnel", "tunnel/proxy.go", "done", "tunnel.py via wakeup WS"),
68
+ Capability("handoff_bundle", "Design handoff bundle", "handoff_bundle.go", "done", "download+extract zip"),
69
+ Capability("design_seed", "Design example seed", "design.SeedExample*", "done", "bundle copy + marker"),
70
+ Capability("sandbox_agent_native", "Provider-native sandbox", "codex_sandbox.go", "partial", "Codex config; Claude flags thin"),
71
+ Capability("sandbox_wrapper", "Optional OS wrapper", "sandbox-runtime", "partial", "bwrap/Seatbelt optional; Win n/a"),
72
+ ]
73
+
74
+
75
+ def by_status() -> dict[str, list[Capability]]:
76
+ out: dict[str, list[Capability]] = {}
77
+ for c in CAPABILITIES:
78
+ out.setdefault(c.status, []).append(c)
79
+ return out
80
+
81
+
82
+ def format_table() -> str:
83
+ order = ["done", "partial", "planned", "hard", "defer", "n/a"]
84
+ lines = [f"{'STATUS':<8} {'ID':<22} {'NAME'}", "-" * 72]
85
+ grouped = by_status()
86
+ for st in order:
87
+ for c in grouped.get(st, []):
88
+ lines.append(f"{c.status:<8} {c.id:<22} {c.name}")
89
+ counts = {st: len(grouped.get(st, [])) for st in order}
90
+ lines.append("-" * 72)
91
+ lines.append("counts: " + ", ".join(f"{st}={counts[st]}" for st in order if counts[st]))
92
+ return "\n".join(lines)
@@ -0,0 +1,321 @@
1
+ """Pooled Cursor/Kiro ACP connections for multi-turn chat sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import threading
7
+ import time
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from .acp_conn import (
12
+ ACPChatConn,
13
+ ACPPromptResult,
14
+ CursorACPConn,
15
+ KiroACPConn,
16
+ cursor_acp_supported,
17
+ kiro_acp_supported,
18
+ )
19
+ from .config import AgentEntry, Config
20
+
21
+ log = logging.getLogger("aicortexpy.acp_pool")
22
+
23
+
24
+ @dataclass
25
+ class _PoolEntry:
26
+ key: str
27
+ provider: str
28
+ chat_id: str
29
+ agent_id: str
30
+ work_dir: str
31
+ model: str
32
+ conn: ACPChatConn | None = None
33
+ last_used: float = field(default_factory=time.time)
34
+ turn_lock: threading.Lock = field(default_factory=threading.Lock)
35
+
36
+
37
+ class ChatACPPool:
38
+ def __init__(
39
+ self,
40
+ *,
41
+ cursor_enabled: bool,
42
+ kiro_enabled: bool,
43
+ idle_timeout: float,
44
+ max_entries: int,
45
+ agents: dict[str, AgentEntry],
46
+ ) -> None:
47
+ self._agents = agents
48
+ self.idle_timeout = idle_timeout if idle_timeout > 0 else 300.0
49
+ self.max_entries = max_entries if max_entries > 0 else 50
50
+ self._lock = threading.Lock()
51
+ self._entries: dict[str, _PoolEntry] = {}
52
+
53
+ cursor_ok = False
54
+ if cursor_enabled:
55
+ entry = agents.get("cursor")
56
+ cursor_ok = bool(entry) and cursor_acp_supported(entry.path)
57
+ self.cursor_enabled = cursor_ok
58
+
59
+ kiro_ok = False
60
+ if kiro_enabled:
61
+ entry = agents.get("kiro")
62
+ kiro_ok = bool(entry) and kiro_acp_supported(entry.path)
63
+ self.kiro_enabled = kiro_ok
64
+
65
+ if self.cursor_enabled:
66
+ log.info("chat ACP pool: cursor enabled")
67
+ if self.kiro_enabled:
68
+ log.info("chat ACP pool: kiro enabled")
69
+
70
+ @classmethod
71
+ def from_config(cls, cfg: Config) -> ChatACPPool:
72
+ return cls(
73
+ cursor_enabled=cfg.cursor_acp_enabled,
74
+ kiro_enabled=cfg.kiro_acp_enabled,
75
+ idle_timeout=cfg.chat_acp_idle_timeout,
76
+ max_entries=cfg.chat_acp_max_connections,
77
+ agents=cfg.agents,
78
+ )
79
+
80
+ def enabled_for(self, provider: str) -> bool:
81
+ if provider == "cursor":
82
+ return self.cursor_enabled
83
+ if provider == "kiro":
84
+ return self.kiro_enabled
85
+ return False
86
+
87
+ def run_prompt(
88
+ self,
89
+ *,
90
+ provider: str,
91
+ chat_session_id: str,
92
+ agent_id: str,
93
+ work_dir: str,
94
+ model: str,
95
+ prompt: str,
96
+ resume_session_id: str,
97
+ custom_args: list[str],
98
+ timeout: float,
99
+ env: dict[str, str] | None = None,
100
+ cancel_event: threading.Event | None = None,
101
+ on_message: Any | None = None,
102
+ idle_watchdog: float = 0,
103
+ system_prompt: str = "",
104
+ ) -> ACPPromptResult:
105
+ key = pool_key(provider, chat_session_id, agent_id, work_dir)
106
+ entry = self._get_or_create(key, provider, chat_session_id, agent_id, work_dir, model)
107
+ with entry.turn_lock:
108
+ if entry.conn is None or entry.model != model:
109
+ if entry.conn is not None:
110
+ try:
111
+ entry.conn.close()
112
+ except Exception:
113
+ pass
114
+ entry.conn = None
115
+ try:
116
+ entry.conn = self._open_conn(
117
+ provider,
118
+ work_dir=work_dir,
119
+ model=model,
120
+ resume_session_id=resume_session_id,
121
+ custom_args=custom_args,
122
+ timeout=timeout,
123
+ env=env,
124
+ )
125
+ entry.model = model
126
+ log.info("%s chat acp pool miss chat=%s", provider, chat_session_id[:8])
127
+ except Exception as e:
128
+ self.remove_entry(key)
129
+ return ACPPromptResult(
130
+ status="failed",
131
+ output="",
132
+ error=f"{provider} chat acp open failed: {e}",
133
+ )
134
+ else:
135
+ log.info("%s chat acp pool hit chat=%s", provider, chat_session_id[:8])
136
+
137
+ entry.last_used = time.time()
138
+ assert entry.conn is not None
139
+
140
+ result = entry.conn.run_prompt(
141
+ prompt,
142
+ timeout=timeout or 20 * 60,
143
+ system_prompt=system_prompt,
144
+ on_message=on_message,
145
+ idle_watchdog=idle_watchdog,
146
+ cancel_event=cancel_event,
147
+ )
148
+
149
+ if not result.session_id and entry.conn.session_id():
150
+ result.session_id = entry.conn.session_id()
151
+
152
+ if _should_evict(result):
153
+ log.info(
154
+ "%s chat acp pool evicting after interrupt status=%s",
155
+ provider,
156
+ result.status,
157
+ )
158
+ self.remove_entry(key)
159
+ elif result.status == "failed" and resume_session_id and not result.session_id:
160
+ log.warning("%s chat acp resume failed, evicting pool entry", provider)
161
+ self.remove_entry(key)
162
+ else:
163
+ entry.last_used = time.time()
164
+ return result
165
+
166
+ def _open_conn(
167
+ self,
168
+ provider: str,
169
+ *,
170
+ work_dir: str,
171
+ model: str,
172
+ resume_session_id: str,
173
+ custom_args: list[str],
174
+ timeout: float,
175
+ env: dict[str, str] | None,
176
+ ) -> ACPChatConn:
177
+ entry = self._agents.get(provider)
178
+ if not entry:
179
+ raise RuntimeError(f"no agent configured for {provider}")
180
+ if provider == "cursor":
181
+ return CursorACPConn.open(
182
+ entry.path,
183
+ cwd=work_dir,
184
+ model=model or entry.model,
185
+ resume_session_id="", # never load for cursor chat
186
+ custom_args=custom_args,
187
+ timeout=timeout,
188
+ env=env,
189
+ )
190
+ if provider == "kiro":
191
+ return KiroACPConn.open(
192
+ entry.path,
193
+ cwd=work_dir,
194
+ model=model or entry.model,
195
+ resume_session_id=resume_session_id,
196
+ custom_args=custom_args,
197
+ timeout=timeout,
198
+ env=env,
199
+ )
200
+ raise RuntimeError(f"chat acp pool: unsupported provider {provider!r}")
201
+
202
+ def _get_or_create(
203
+ self,
204
+ key: str,
205
+ provider: str,
206
+ chat_id: str,
207
+ agent_id: str,
208
+ work_dir: str,
209
+ model: str,
210
+ ) -> _PoolEntry:
211
+ stale: list[ACPChatConn] = []
212
+ with self._lock:
213
+ stale.extend(self._take_idle_locked(time.time()))
214
+ if key in self._entries:
215
+ e = self._entries[key]
216
+ else:
217
+ if len(self._entries) >= self.max_entries:
218
+ stale.extend(self._take_oldest_locked())
219
+ e = _PoolEntry(
220
+ key=key,
221
+ provider=provider,
222
+ chat_id=chat_id,
223
+ agent_id=agent_id,
224
+ work_dir=work_dir,
225
+ model=model,
226
+ )
227
+ self._entries[key] = e
228
+ _close_conns(stale)
229
+ return e
230
+
231
+ def remove_entry(self, key: str) -> None:
232
+ with self._lock:
233
+ e = self._entries.pop(key, None)
234
+ conn = e.conn if e else None
235
+ if e:
236
+ e.conn = None
237
+ if conn is not None:
238
+ try:
239
+ conn.close()
240
+ except Exception:
241
+ pass
242
+
243
+ def evict_chat_session(self, chat_session_id: str) -> None:
244
+ stale: list[ACPChatConn] = []
245
+ with self._lock:
246
+ for key, e in list(self._entries.items()):
247
+ if e.chat_id != chat_session_id:
248
+ continue
249
+ if e.conn is not None:
250
+ stale.append(e.conn)
251
+ e.conn = None
252
+ del self._entries[key]
253
+ _close_conns(stale)
254
+
255
+ def close_all(self) -> None:
256
+ with self._lock:
257
+ stale = [e.conn for e in self._entries.values() if e.conn is not None]
258
+ for e in self._entries.values():
259
+ e.conn = None
260
+ self._entries.clear()
261
+ _close_conns(stale)
262
+
263
+ def _take_idle_locked(self, now: float) -> list[ACPChatConn]:
264
+ stale: list[ACPChatConn] = []
265
+ for key, e in list(self._entries.items()):
266
+ if now - e.last_used <= self.idle_timeout:
267
+ continue
268
+ if e.conn is not None:
269
+ stale.append(e.conn)
270
+ e.conn = None
271
+ del self._entries[key]
272
+ log.info(
273
+ "chat acp pool evicted idle connection provider=%s chat=%s",
274
+ e.provider,
275
+ e.chat_id[:8],
276
+ )
277
+ return stale
278
+
279
+ def _take_oldest_locked(self) -> list[ACPChatConn]:
280
+ if not self._entries:
281
+ return []
282
+ oldest_key = min(self._entries, key=lambda k: self._entries[k].last_used)
283
+ e = self._entries.pop(oldest_key)
284
+ log.info(
285
+ "chat acp pool evicted oldest connection provider=%s chat=%s",
286
+ e.provider,
287
+ e.chat_id[:8],
288
+ )
289
+ if e.conn is None:
290
+ return []
291
+ conn = e.conn
292
+ e.conn = None
293
+ return [conn]
294
+
295
+
296
+ def pool_key(provider: str, chat_session_id: str, agent_id: str, work_dir: str) -> str:
297
+ return "\x00".join([provider, chat_session_id, agent_id, work_dir])
298
+
299
+
300
+ def agent_id_from_task(task: Any) -> str:
301
+ agent = getattr(task, "agent", None) or {}
302
+ if isinstance(agent, dict):
303
+ return str(agent.get("id") or "")
304
+ return ""
305
+
306
+
307
+ def skip_cursor_chat_session_resume(provider: str, chat_session_id: str) -> bool:
308
+ """Cursor must not session/load a completed chat ACP session."""
309
+ return provider == "cursor" and bool(str(chat_session_id or "").strip())
310
+
311
+
312
+ def _should_evict(result: ACPPromptResult) -> bool:
313
+ return result.status in ("aborted", "cancelled", "timeout", "idle_watchdog")
314
+
315
+
316
+ def _close_conns(conns: list[ACPChatConn]) -> None:
317
+ for c in conns:
318
+ try:
319
+ c.close()
320
+ except Exception:
321
+ pass
aicortexpy/cli.py ADDED
@@ -0,0 +1,133 @@
1
+ """CLI entry: python -m aicortexpy <command>."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+
9
+ from . import __version__
10
+ from .capabilities import format_table
11
+ from .config import load_config
12
+ from .daemon import Daemon
13
+ from .platform_info import platform_label
14
+ from .process import log_path, spawn_background, status_text, stop_daemon
15
+ from .sandbox import describe_default_strategy
16
+
17
+
18
+ def _setup_logging(verbose: bool = False) -> None:
19
+ level = logging.DEBUG if verbose else logging.INFO
20
+ logging.basicConfig(
21
+ level=level,
22
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
23
+ datefmt="%H:%M:%S",
24
+ )
25
+
26
+
27
+ def build_parser() -> argparse.ArgumentParser:
28
+ p = argparse.ArgumentParser(
29
+ prog="python -m aicortexpy",
30
+ description="AICortex Python local agent runtime daemon (MVP)",
31
+ )
32
+ p.add_argument("--version", action="version", version=f"aicortexpy {__version__}")
33
+ p.add_argument("--profile", default="", help="CLI profile name (default profile if empty)")
34
+ p.add_argument("-v", "--verbose", action="store_true")
35
+
36
+ sub = p.add_subparsers(dest="command", required=True)
37
+
38
+ start = sub.add_parser("start", help="Start daemon in background")
39
+ start.add_argument(
40
+ "--foreground",
41
+ action="store_true",
42
+ help="Run in the current terminal instead of detaching",
43
+ )
44
+
45
+ sub.add_parser("run", help="Run daemon in foreground (used by start / service)")
46
+ sub.add_parser("stop", help="Stop the running daemon")
47
+ sub.add_parser("status", help="Show daemon status")
48
+ sub.add_parser("restart", help="Restart daemon")
49
+ sub.add_parser(
50
+ "capabilities",
51
+ help="List Go daemon capabilities vs Python implementation status",
52
+ )
53
+ sub.add_parser(
54
+ "sandbox-info",
55
+ help="Show host OS sandbox strategy (Win/macOS/Linux)",
56
+ )
57
+
58
+ du = sub.add_parser(
59
+ "disk-usage",
60
+ help="Scan workspaces root disk usage (read-only)",
61
+ )
62
+ du.add_argument("--json", action="store_true", help="Emit JSON report")
63
+ du.add_argument("--top", type=int, default=20, help="Show top N task dirs")
64
+
65
+ return p
66
+
67
+
68
+ def main(argv: list[str] | None = None) -> int:
69
+ parser = build_parser()
70
+ args = parser.parse_args(argv)
71
+ _setup_logging(args.verbose)
72
+
73
+ if args.command == "capabilities":
74
+ print(format_table())
75
+ return 0
76
+
77
+ if args.command == "sandbox-info":
78
+ print(f"platform: {platform_label()}")
79
+ print(describe_default_strategy())
80
+ return 0
81
+
82
+ if args.command == "disk-usage":
83
+ import json
84
+
85
+ from .diskusage import format_report_table, report_to_dict, scan_disk_usage
86
+
87
+ cfg = load_config(args.profile, require_agents=False)
88
+ report = scan_disk_usage(cfg.workspaces_root)
89
+ if args.json:
90
+ print(json.dumps(report_to_dict(report), ensure_ascii=False, indent=2))
91
+ else:
92
+ print(format_report_table(report, top=args.top))
93
+ return 0
94
+
95
+ if args.command == "status":
96
+ cfg = load_config(args.profile, require_agents=False)
97
+ print(status_text(cfg))
98
+ return 0
99
+
100
+ if args.command == "stop":
101
+ cfg = load_config(args.profile, require_agents=False)
102
+ print(stop_daemon(cfg))
103
+ return 0
104
+
105
+ if args.command == "restart":
106
+ cfg = load_config(args.profile, require_agents=False)
107
+ print(stop_daemon(cfg))
108
+ cfg = load_config(args.profile, require_agents=True)
109
+ pid = spawn_background(cfg)
110
+ print(f"restarted pid={pid}")
111
+ print(f"logs: {log_path(cfg)}")
112
+ return 0
113
+
114
+ if args.command == "start":
115
+ cfg = load_config(args.profile, require_agents=True)
116
+ if args.foreground:
117
+ return Daemon(cfg).run()
118
+ pid = spawn_background(cfg)
119
+ print(f"started pid={pid}")
120
+ print(f"health: http://127.0.0.1:{cfg.health_port}/health")
121
+ print(f"logs: {log_path(cfg)}")
122
+ return 0
123
+
124
+ if args.command == "run":
125
+ cfg = load_config(args.profile, require_agents=True)
126
+ return Daemon(cfg).run()
127
+
128
+ parser.print_help()
129
+ return 2
130
+
131
+
132
+ if __name__ == "__main__":
133
+ sys.exit(main())