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
aicortexpy/config.py ADDED
@@ -0,0 +1,298 @@
1
+ """Daemon configuration — mirrors the Go daemon env/config surface for MVP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import platform
8
+ import shutil
9
+ import socket
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ DEFAULT_SERVER_URL = "http://localhost:8080"
17
+ DEFAULT_POLL_INTERVAL = 30.0
18
+ DEFAULT_HEARTBEAT_INTERVAL = 15.0
19
+ DEFAULT_AGENT_TIMEOUT = 2 * 60 * 60.0
20
+ DEFAULT_HEALTH_PORT = 19514 # align with Go daemon / `aicortex daemon status`
21
+ DEFAULT_MAX_CONCURRENT = 4
22
+ DEFAULT_GC_INTERVAL = 3600.0
23
+ DEFAULT_GC_TTL = 86400.0
24
+ DEFAULT_GC_ORPHAN_TTL = 259200.0
25
+ DEFAULT_GC_ARTIFACT_TTL = 43200.0
26
+ DEFAULT_CHAT_ACP_IDLE_TIMEOUT = 300.0
27
+ DEFAULT_CHAT_ACP_MAX_CONNECTIONS = 50
28
+ DEFAULT_AGENT_IDLE_WATCHDOG = 300.0 # 5m; 0 disables
29
+ DEFAULT_CODEX_SEMANTIC_INACTIVITY = 600.0 # 10m; 0 disables
30
+ CLI_VERSION = "0.1.0-python"
31
+
32
+ # provider -> (env path override, default binary name, model env)
33
+ # Keep in sync with server/internal/daemon/config.go probe list.
34
+ AGENT_PROBES: list[tuple[str, str, str, str]] = [
35
+ ("claude", "AICORTEX_CLAUDE_PATH", "claude", "AICORTEX_CLAUDE_MODEL"),
36
+ ("codex", "AICORTEX_CODEX_PATH", "codex", "AICORTEX_CODEX_MODEL"),
37
+ ("opencode", "AICORTEX_OPENCODE_PATH", "opencode", "AICORTEX_OPENCODE_MODEL"),
38
+ ("openclaw", "AICORTEX_OPENCLAW_PATH", "openclaw", "AICORTEX_OPENCLAW_MODEL"),
39
+ ("hermes", "AICORTEX_HERMES_PATH", "hermes", "AICORTEX_HERMES_MODEL"),
40
+ ("gemini", "AICORTEX_GEMINI_PATH", "gemini", "AICORTEX_GEMINI_MODEL"),
41
+ ("pi", "AICORTEX_PI_PATH", "pi", "AICORTEX_PI_MODEL"),
42
+ ("cursor", "AICORTEX_CURSOR_PATH", "cursor-agent", "AICORTEX_CURSOR_MODEL"),
43
+ ("copilot", "AICORTEX_COPILOT_PATH", "copilot", "AICORTEX_COPILOT_MODEL"),
44
+ ("kimi", "AICORTEX_KIMI_PATH", "kimi", "AICORTEX_KIMI_MODEL"),
45
+ ("kiro", "AICORTEX_KIRO_PATH", "kiro-cli", "AICORTEX_KIRO_MODEL"),
46
+ ("dsh", "AICORTEX_DSH_PATH", "dsh", "AICORTEX_DSH_MODEL"),
47
+ ]
48
+
49
+
50
+ @dataclass
51
+ class AgentEntry:
52
+ path: str
53
+ model: str = ""
54
+
55
+
56
+ @dataclass
57
+ class CLIConfig:
58
+ server_url: str = ""
59
+ app_url: str = ""
60
+ workspace_id: str = ""
61
+ token: str = ""
62
+
63
+
64
+ @dataclass
65
+ class Config:
66
+ server_base_url: str
67
+ token: str
68
+ daemon_id: str
69
+ device_name: str
70
+ cli_version: str = CLI_VERSION
71
+ agents: dict[str, AgentEntry] = field(default_factory=dict)
72
+ workspaces_root: Path = field(default_factory=lambda: Path.home() / "aicortex_workspaces")
73
+ health_port: int = DEFAULT_HEALTH_PORT
74
+ poll_interval: float = DEFAULT_POLL_INTERVAL
75
+ heartbeat_interval: float = DEFAULT_HEARTBEAT_INTERVAL
76
+ agent_timeout: float = DEFAULT_AGENT_TIMEOUT
77
+ max_concurrent_tasks: int = DEFAULT_MAX_CONCURRENT
78
+ profile: str = ""
79
+ state_dir: Path = field(default_factory=lambda: Path.home() / ".aicortex")
80
+ gc_enabled: bool = True
81
+ gc_interval: float = DEFAULT_GC_INTERVAL
82
+ gc_ttl: float = DEFAULT_GC_TTL
83
+ gc_orphan_ttl: float = DEFAULT_GC_ORPHAN_TTL
84
+ gc_artifact_ttl: float = DEFAULT_GC_ARTIFACT_TTL
85
+ cursor_acp_enabled: bool = True
86
+ kiro_acp_enabled: bool = True
87
+ chat_acp_idle_timeout: float = DEFAULT_CHAT_ACP_IDLE_TIMEOUT
88
+ chat_acp_max_connections: int = DEFAULT_CHAT_ACP_MAX_CONNECTIONS
89
+ agent_idle_watchdog: float = DEFAULT_AGENT_IDLE_WATCHDOG
90
+ codex_semantic_inactivity: float = DEFAULT_CODEX_SEMANTIC_INACTIVITY
91
+
92
+
93
+ def profile_dir(profile: str = "") -> Path:
94
+ home = Path.home() / ".aicortex"
95
+ if profile:
96
+ return home / "profiles" / profile
97
+ return home
98
+
99
+
100
+ def cli_config_path(profile: str = "") -> Path:
101
+ return profile_dir(profile) / "config.json"
102
+
103
+
104
+ def load_cli_config(profile: str = "") -> CLIConfig:
105
+ path = cli_config_path(profile)
106
+ if not path.is_file():
107
+ return CLIConfig()
108
+ data = json.loads(path.read_text(encoding="utf-8"))
109
+ return CLIConfig(
110
+ server_url=str(data.get("server_url") or ""),
111
+ app_url=str(data.get("app_url") or ""),
112
+ workspace_id=str(data.get("workspace_id") or ""),
113
+ token=str(data.get("token") or ""),
114
+ )
115
+
116
+
117
+ def normalize_http_base(url: str) -> str:
118
+ u = (url or "").strip().rstrip("/")
119
+ if u.startswith("ws://"):
120
+ u = "http://" + u[len("ws://") :]
121
+ elif u.startswith("wss://"):
122
+ u = "https://" + u[len("wss://") :]
123
+ # Strip trailing /ws used by Go default
124
+ if u.endswith("/ws"):
125
+ u = u[: -len("/ws")]
126
+ return u.rstrip("/")
127
+
128
+
129
+ def _env_float(name: str, default: float) -> float:
130
+ raw = os.environ.get(name, "").strip()
131
+ if not raw:
132
+ return default
133
+ return float(raw)
134
+
135
+
136
+ def _env_int(name: str, default: int) -> int:
137
+ raw = os.environ.get(name, "").strip()
138
+ if not raw:
139
+ return default
140
+ return int(raw)
141
+
142
+
143
+ def _env_bool(name: str, default: bool) -> bool:
144
+ raw = os.environ.get(name)
145
+ if raw is None or not str(raw).strip():
146
+ return default
147
+ return str(raw).strip().lower() in {"1", "true", "yes", "on"}
148
+
149
+
150
+ def discover_agents() -> dict[str, AgentEntry]:
151
+ agents: dict[str, AgentEntry] = {}
152
+ for provider, path_env, default_bin, model_env in AGENT_PROBES:
153
+ override = os.environ.get(path_env, "").strip()
154
+ path = override or shutil.which(default_bin) or ""
155
+ if not path:
156
+ continue
157
+ if override and not Path(override).exists() and not shutil.which(override):
158
+ continue
159
+ resolved = shutil.which(path) or path
160
+ agents[provider] = AgentEntry(
161
+ path=resolved,
162
+ model=os.environ.get(model_env, "").strip(),
163
+ )
164
+ return agents
165
+
166
+
167
+ def default_daemon_id() -> str:
168
+ host = socket.gethostname() or "local"
169
+ # Stable-ish id per machine for python daemon
170
+ seed = f"python-daemon:{host}:{platform.node()}"
171
+ return "py-" + uuid.uuid5(uuid.NAMESPACE_DNS, seed).hex[:12]
172
+
173
+
174
+ def health_port_for_profile(profile: str = "") -> int:
175
+ """Match Go healthPortForProfile so `aicortex daemon status` finds us."""
176
+ if not profile:
177
+ return DEFAULT_HEALTH_PORT
178
+ h = sum(ord(c) for c in profile)
179
+ return DEFAULT_HEALTH_PORT + 1 + (h % 1000)
180
+
181
+
182
+ def load_config(profile: str = "", *, require_agents: bool = True) -> Config:
183
+ cli = load_cli_config(profile)
184
+ server = normalize_http_base(
185
+ os.environ.get("AICORTEX_SERVER_URL")
186
+ or cli.server_url
187
+ or DEFAULT_SERVER_URL
188
+ )
189
+ token = os.environ.get("AICORTEX_TOKEN") or cli.token
190
+ if not token:
191
+ raise SystemExit(
192
+ "No auth token. Run `aicortex setup`/`login` first, or set AICORTEX_TOKEN."
193
+ )
194
+
195
+ agents = discover_agents()
196
+ if require_agents and not agents:
197
+ raise SystemExit(
198
+ "No agent CLI found. Install claude/codex/... and ensure it is on PATH, "
199
+ "or set AICORTEX_CLAUDE_PATH / AICORTEX_CODEX_PATH."
200
+ )
201
+
202
+ host = socket.gethostname() or "local-machine"
203
+ state = profile_dir(profile)
204
+ workspaces = Path(
205
+ os.environ.get("AICORTEX_WORKSPACES_ROOT")
206
+ or (Path.home() / "aicortex_workspaces")
207
+ )
208
+
209
+ return Config(
210
+ server_base_url=server,
211
+ token=token,
212
+ daemon_id=os.environ.get("aicortex_ID") or default_daemon_id(),
213
+ device_name=os.environ.get("aicortex_DEVICE_NAME") or host,
214
+ cli_version=os.environ.get("AICORTEX_CLI_VERSION") or CLI_VERSION,
215
+ agents=agents,
216
+ workspaces_root=workspaces,
217
+ health_port=_env_int(
218
+ "aicortex_HEALTH_PORT", health_port_for_profile(profile)
219
+ ),
220
+ poll_interval=_env_float("aicortex_POLL_INTERVAL", DEFAULT_POLL_INTERVAL),
221
+ heartbeat_interval=_env_float(
222
+ "aicortex_HEARTBEAT_INTERVAL", DEFAULT_HEARTBEAT_INTERVAL
223
+ ),
224
+ agent_timeout=_env_float("AICORTEX_AGENT_TIMEOUT", DEFAULT_AGENT_TIMEOUT),
225
+ max_concurrent_tasks=_env_int(
226
+ "aicortex_MAX_CONCURRENT_TASKS", DEFAULT_MAX_CONCURRENT
227
+ ),
228
+ profile=profile,
229
+ state_dir=state,
230
+ gc_enabled=_env_bool("AICORTEX_GC_ENABLED", True),
231
+ gc_interval=_env_float("AICORTEX_GC_INTERVAL", DEFAULT_GC_INTERVAL),
232
+ gc_ttl=_env_float("AICORTEX_GC_TTL", DEFAULT_GC_TTL),
233
+ gc_orphan_ttl=_env_float("AICORTEX_GC_ORPHAN_TTL", DEFAULT_GC_ORPHAN_TTL),
234
+ gc_artifact_ttl=_env_float("AICORTEX_GC_ARTIFACT_TTL", DEFAULT_GC_ARTIFACT_TTL),
235
+ cursor_acp_enabled=_env_bool("AICORTEX_CURSOR_ACP_ENABLED", True),
236
+ kiro_acp_enabled=_env_bool("AICORTEX_KIRO_ACP_ENABLED", True),
237
+ chat_acp_idle_timeout=_env_float(
238
+ "AICORTEX_CHAT_ACP_IDLE_TIMEOUT", DEFAULT_CHAT_ACP_IDLE_TIMEOUT
239
+ ),
240
+ chat_acp_max_connections=_env_int(
241
+ "AICORTEX_CHAT_ACP_MAX_CONNECTIONS", DEFAULT_CHAT_ACP_MAX_CONNECTIONS
242
+ ),
243
+ agent_idle_watchdog=_env_float(
244
+ "AICORTEX_AGENT_IDLE_WATCHDOG", DEFAULT_AGENT_IDLE_WATCHDOG
245
+ ),
246
+ codex_semantic_inactivity=_env_float(
247
+ "AICORTEX_CODEX_SEMANTIC_INACTIVITY_TIMEOUT",
248
+ DEFAULT_CODEX_SEMANTIC_INACTIVITY,
249
+ ),
250
+ )
251
+
252
+
253
+ def detect_local_timezone() -> str:
254
+ try:
255
+ return str(__import__("datetime").datetime.now().astimezone().tzinfo)
256
+ except Exception:
257
+ return "UTC"
258
+
259
+
260
+ def detect_agent_version(executable: str) -> str:
261
+ """Best-effort `cli --version` parse; falls back to 'unknown'."""
262
+ import re
263
+ import subprocess
264
+
265
+ for args in ([executable, "--version"], [executable, "version"], [executable, "-V"]):
266
+ try:
267
+ proc = subprocess.run(
268
+ args,
269
+ capture_output=True,
270
+ text=True,
271
+ timeout=15,
272
+ check=False,
273
+ )
274
+ out = (proc.stdout or "") + "\n" + (proc.stderr or "")
275
+ m = re.search(r"(\d+\.\d+(?:\.\d+)?)", out)
276
+ if m:
277
+ return m.group(1)
278
+ except Exception:
279
+ continue
280
+ return "unknown"
281
+
282
+
283
+ def as_register_runtimes(cfg: Config) -> list[dict[str, Any]]:
284
+ runtimes: list[dict[str, Any]] = []
285
+ for name, entry in cfg.agents.items():
286
+ version = detect_agent_version(entry.path)
287
+ display = name[:1].upper() + name[1:]
288
+ if cfg.device_name:
289
+ display = f"{display} ({cfg.device_name})"
290
+ runtimes.append(
291
+ {
292
+ "name": display,
293
+ "type": name,
294
+ "version": version,
295
+ "status": "online",
296
+ }
297
+ )
298
+ return runtimes