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.
- aicortexpy/__init__.py +3 -0
- aicortexpy/__main__.py +5 -0
- aicortexpy/acp_client.py +559 -0
- aicortexpy/acp_conn.py +853 -0
- aicortexpy/acp_errors.py +107 -0
- aicortexpy/agent_env.py +88 -0
- aicortexpy/artifact_ws.py +233 -0
- aicortexpy/artifacts.py +67 -0
- aicortexpy/attachments.py +85 -0
- aicortexpy/auto_commit.py +106 -0
- aicortexpy/capabilities.py +92 -0
- aicortexpy/chat_acp_pool.py +321 -0
- aicortexpy/cli.py +133 -0
- aicortexpy/client.py +438 -0
- aicortexpy/codex_config.py +98 -0
- aicortexpy/config.py +298 -0
- aicortexpy/daemon.py +677 -0
- aicortexpy/data/default_skills/agent-memory/SKILL.md +67 -0
- aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +144 -0
- aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +145 -0
- aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +87 -0
- aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +96 -0
- aicortexpy/data/default_skills/aicortex-basics/SKILL.md +10 -0
- aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +118 -0
- aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +100 -0
- aicortexpy/data/default_skills/aicortex-graph/SKILL.md +30 -0
- aicortexpy/data/default_skills/graphify/SKILL.md +97 -0
- aicortexpy/data/default_skills/question-form/SKILL.md +162 -0
- aicortexpy/data/default_skills/test-report-generator/SKILL.md +179 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +146 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +275 -0
- aicortexpy/design_seed.py +59 -0
- aicortexpy/diskusage.py +209 -0
- aicortexpy/execenv.py +264 -0
- aicortexpy/gc.py +286 -0
- aicortexpy/handoff_bundle.py +65 -0
- aicortexpy/health.py +178 -0
- aicortexpy/local_skills.py +157 -0
- aicortexpy/models.py +385 -0
- aicortexpy/openclaw_config.py +153 -0
- aicortexpy/pinned_workdir.py +60 -0
- aicortexpy/platform_info.py +61 -0
- aicortexpy/poisoned.py +44 -0
- aicortexpy/process.py +168 -0
- aicortexpy/procutil.py +61 -0
- aicortexpy/prompt.py +235 -0
- aicortexpy/repo.py +462 -0
- aicortexpy/runner.py +969 -0
- aicortexpy/runtime_config.py +540 -0
- aicortexpy/sandbox.py +264 -0
- aicortexpy/stream.py +422 -0
- aicortexpy/terminal.py +147 -0
- aicortexpy/tunnel.py +82 -0
- aicortexpy/wakeup.py +222 -0
- aicortexpy-0.1.0.dist-info/METADATA +106 -0
- aicortexpy-0.1.0.dist-info/RECORD +58 -0
- aicortexpy-0.1.0.dist-info/WHEEL +5 -0
- 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
|