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/health.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Local health HTTP endpoint (single-instance lock via bind)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger("aicortexpy.health")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HealthServer:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
port: int,
|
|
21
|
+
snapshot: Callable[[], dict[str, Any]],
|
|
22
|
+
*,
|
|
23
|
+
workspaces_root: Path | None = None,
|
|
24
|
+
on_shutdown: Callable[[], None] | None = None,
|
|
25
|
+
repo_allowlist: Any = None,
|
|
26
|
+
client: Any = None,
|
|
27
|
+
):
|
|
28
|
+
self.port = port
|
|
29
|
+
self.snapshot = snapshot
|
|
30
|
+
self.workspaces_root = workspaces_root
|
|
31
|
+
self.on_shutdown = on_shutdown
|
|
32
|
+
self.repo_allowlist = repo_allowlist
|
|
33
|
+
self.client = client
|
|
34
|
+
self._httpd: ThreadingHTTPServer | None = None
|
|
35
|
+
self._thread: threading.Thread | None = None
|
|
36
|
+
self.started_at = time.time()
|
|
37
|
+
|
|
38
|
+
def start(self) -> None:
|
|
39
|
+
snapshot = self.snapshot
|
|
40
|
+
started_at = self.started_at
|
|
41
|
+
workspaces_root = self.workspaces_root
|
|
42
|
+
on_shutdown = self.on_shutdown
|
|
43
|
+
repo_allowlist = self.repo_allowlist
|
|
44
|
+
client = self.client
|
|
45
|
+
|
|
46
|
+
class Handler(BaseHTTPRequestHandler):
|
|
47
|
+
def do_GET(self): # noqa: N802
|
|
48
|
+
if self.path.split("?")[0] not in ("/health", "/"):
|
|
49
|
+
self.send_response(404)
|
|
50
|
+
self.end_headers()
|
|
51
|
+
return
|
|
52
|
+
body = snapshot()
|
|
53
|
+
body.setdefault("status", "running")
|
|
54
|
+
body.setdefault("pid", os.getpid())
|
|
55
|
+
body.setdefault("uptime", f"{int(time.time() - started_at)}s")
|
|
56
|
+
raw = json.dumps(body).encode("utf-8")
|
|
57
|
+
self.send_response(200)
|
|
58
|
+
self.send_header("Content-Type", "application/json")
|
|
59
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
60
|
+
self.end_headers()
|
|
61
|
+
self.wfile.write(raw)
|
|
62
|
+
|
|
63
|
+
def do_POST(self): # noqa: N802
|
|
64
|
+
path = self.path.split("?")[0]
|
|
65
|
+
if path == "/shutdown":
|
|
66
|
+
self.send_response(200)
|
|
67
|
+
self.send_header("Content-Type", "application/json")
|
|
68
|
+
self.end_headers()
|
|
69
|
+
self.wfile.write(b'{"ok":true}')
|
|
70
|
+
if on_shutdown:
|
|
71
|
+
threading.Thread(target=on_shutdown, daemon=True).start()
|
|
72
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start()
|
|
73
|
+
return
|
|
74
|
+
if path == "/repo/checkout":
|
|
75
|
+
self._repo_checkout()
|
|
76
|
+
return
|
|
77
|
+
self.send_response(404)
|
|
78
|
+
self.end_headers()
|
|
79
|
+
|
|
80
|
+
def _repo_checkout(self) -> None:
|
|
81
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
82
|
+
raw_body = self.rfile.read(length) if length else b"{}"
|
|
83
|
+
try:
|
|
84
|
+
req = json.loads(raw_body.decode("utf-8"))
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
self.send_error(400, "invalid json")
|
|
87
|
+
return
|
|
88
|
+
url = str(req.get("url") or "")
|
|
89
|
+
workspace_id = str(req.get("workspace_id") or "")
|
|
90
|
+
workdir = str(req.get("workdir") or "")
|
|
91
|
+
ref = str(req.get("ref") or "")
|
|
92
|
+
agent_name = str(req.get("agent_name") or req.get("agentName") or "agent")
|
|
93
|
+
task_id = str(req.get("task_id") or req.get("taskId") or "")
|
|
94
|
+
co = req.get("co_authored_by_enabled")
|
|
95
|
+
if co is None:
|
|
96
|
+
co = req.get("coAuthoredByEnabled")
|
|
97
|
+
co_authored_by = True if co is None else bool(co)
|
|
98
|
+
if not url or not workspace_id or not workdir:
|
|
99
|
+
self.send_error(400, "url, workspace_id, workdir required")
|
|
100
|
+
return
|
|
101
|
+
if workspaces_root is None:
|
|
102
|
+
self.send_error(500, "repo cache not initialized")
|
|
103
|
+
return
|
|
104
|
+
from .repo import checkout_api
|
|
105
|
+
|
|
106
|
+
result = checkout_api(
|
|
107
|
+
workspaces_root,
|
|
108
|
+
url=url,
|
|
109
|
+
workspace_id=workspace_id,
|
|
110
|
+
workdir=workdir,
|
|
111
|
+
ref=ref,
|
|
112
|
+
allowlist=repo_allowlist,
|
|
113
|
+
client=client,
|
|
114
|
+
agent_name=agent_name,
|
|
115
|
+
task_id=task_id,
|
|
116
|
+
co_authored_by=co_authored_by,
|
|
117
|
+
)
|
|
118
|
+
raw = json.dumps(result).encode("utf-8")
|
|
119
|
+
if result.get("ok"):
|
|
120
|
+
code = 200
|
|
121
|
+
elif int(result.get("status") or 0) == 400:
|
|
122
|
+
code = 400
|
|
123
|
+
else:
|
|
124
|
+
code = 500
|
|
125
|
+
self.send_response(code)
|
|
126
|
+
self.send_header("Content-Type", "application/json")
|
|
127
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
128
|
+
self.end_headers()
|
|
129
|
+
self.wfile.write(raw)
|
|
130
|
+
|
|
131
|
+
def log_message(self, fmt: str, *args: Any) -> None:
|
|
132
|
+
log.debug("health: " + fmt, *args)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
httpd = ThreadingHTTPServer(("127.0.0.1", self.port), Handler)
|
|
136
|
+
except OSError as e:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"another daemon is already running on 127.0.0.1:{self.port}: {e}"
|
|
139
|
+
) from e
|
|
140
|
+
self._httpd = httpd
|
|
141
|
+
t = threading.Thread(target=httpd.serve_forever, name="health", daemon=True)
|
|
142
|
+
t.start()
|
|
143
|
+
self._thread = t
|
|
144
|
+
log.info("health listening on 127.0.0.1:%s", self.port)
|
|
145
|
+
|
|
146
|
+
def stop(self) -> None:
|
|
147
|
+
if self._httpd:
|
|
148
|
+
self._httpd.shutdown()
|
|
149
|
+
self._httpd.server_close()
|
|
150
|
+
self._httpd = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def fetch_health(port: int, timeout: float = 2.0) -> dict[str, Any] | None:
|
|
154
|
+
import urllib.request
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
with urllib.request.urlopen(
|
|
158
|
+
f"http://127.0.0.1:{port}/health", timeout=timeout
|
|
159
|
+
) as resp:
|
|
160
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def request_shutdown(port: int, timeout: float = 5.0) -> bool:
|
|
166
|
+
import urllib.request
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
req = urllib.request.Request(
|
|
170
|
+
f"http://127.0.0.1:{port}/shutdown",
|
|
171
|
+
data=b"{}",
|
|
172
|
+
method="POST",
|
|
173
|
+
headers={"Content-Type": "application/json"},
|
|
174
|
+
)
|
|
175
|
+
with urllib.request.urlopen(req, timeout=timeout):
|
|
176
|
+
return True
|
|
177
|
+
except Exception:
|
|
178
|
+
return False
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Scan / import runtime-local skills (aligned with Go local_skills.go)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("aicortexpy.local_skills")
|
|
11
|
+
|
|
12
|
+
MAX_FILE_SIZE = 1 << 20
|
|
13
|
+
MAX_BUNDLE_SIZE = 8 << 20
|
|
14
|
+
MAX_FILE_COUNT = 128
|
|
15
|
+
MAX_DEPTH = 4
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def skill_root_for_provider(provider: str) -> tuple[Path | None, bool]:
|
|
19
|
+
home = Path.home()
|
|
20
|
+
if provider in ("claude", "cursor", "kiro"):
|
|
21
|
+
mapping = {
|
|
22
|
+
"claude": home / ".claude" / "skills",
|
|
23
|
+
"cursor": home / ".cursor" / "skills",
|
|
24
|
+
"kiro": home / ".kiro" / "skills",
|
|
25
|
+
}
|
|
26
|
+
return mapping[provider], True
|
|
27
|
+
if provider == "codex":
|
|
28
|
+
codex_home = os.environ.get("CODEX_HOME", "").strip()
|
|
29
|
+
root = Path(codex_home) if codex_home else home / ".codex"
|
|
30
|
+
return root / "skills", True
|
|
31
|
+
if provider == "copilot":
|
|
32
|
+
return home / ".copilot" / "skills", True
|
|
33
|
+
if provider == "opencode":
|
|
34
|
+
return home / ".config" / "opencode" / "skills", True
|
|
35
|
+
if provider == "openclaw":
|
|
36
|
+
return home / ".openclaw" / "skills", True
|
|
37
|
+
if provider == "pi":
|
|
38
|
+
return home / ".pi" / "agent" / "skills", True
|
|
39
|
+
return None, False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def list_runtime_local_skills(provider: str) -> tuple[list[dict[str, Any]], bool]:
|
|
43
|
+
root, supported = skill_root_for_provider(provider)
|
|
44
|
+
if not supported or root is None:
|
|
45
|
+
return [], False
|
|
46
|
+
if not root.is_dir():
|
|
47
|
+
return [], True
|
|
48
|
+
skills: list[dict[str, Any]] = []
|
|
49
|
+
for skill_md in _find_skill_mds(root, max_depth=MAX_DEPTH):
|
|
50
|
+
skill_dir = skill_md.parent
|
|
51
|
+
rel = skill_dir.relative_to(root).as_posix()
|
|
52
|
+
name = skill_dir.name
|
|
53
|
+
desc = _read_description(skill_md)
|
|
54
|
+
files = list(skill_dir.rglob("*"))
|
|
55
|
+
file_count = sum(1 for f in files if f.is_file())
|
|
56
|
+
skills.append(
|
|
57
|
+
{
|
|
58
|
+
"key": rel,
|
|
59
|
+
"name": name,
|
|
60
|
+
"description": desc,
|
|
61
|
+
"source_path": str(skill_dir),
|
|
62
|
+
"provider": provider,
|
|
63
|
+
"file_count": file_count,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
skills.sort(key=lambda s: s["key"])
|
|
67
|
+
return skills, True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_runtime_local_skill_bundle(
|
|
71
|
+
provider: str, skill_key: str
|
|
72
|
+
) -> tuple[dict[str, Any] | None, bool, str]:
|
|
73
|
+
root, supported = skill_root_for_provider(provider)
|
|
74
|
+
if not supported or root is None:
|
|
75
|
+
return None, False, "provider does not support local skills"
|
|
76
|
+
skill_dir = (root / skill_key).resolve()
|
|
77
|
+
try:
|
|
78
|
+
skill_dir.relative_to(root.resolve())
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None, True, "skill key escapes skills root"
|
|
81
|
+
skill_md = skill_dir / "SKILL.md"
|
|
82
|
+
if not skill_md.is_file():
|
|
83
|
+
return None, True, "SKILL.md not found"
|
|
84
|
+
content = skill_md.read_text(encoding="utf-8", errors="replace")
|
|
85
|
+
files: list[dict[str, str]] = []
|
|
86
|
+
total = len(content.encode("utf-8"))
|
|
87
|
+
count = 0
|
|
88
|
+
for path in skill_dir.rglob("*"):
|
|
89
|
+
if not path.is_file() or path.name == "SKILL.md":
|
|
90
|
+
continue
|
|
91
|
+
count += 1
|
|
92
|
+
if count > MAX_FILE_COUNT:
|
|
93
|
+
break
|
|
94
|
+
try:
|
|
95
|
+
size = path.stat().st_size
|
|
96
|
+
except OSError:
|
|
97
|
+
continue
|
|
98
|
+
if size > MAX_FILE_SIZE:
|
|
99
|
+
continue
|
|
100
|
+
total += size
|
|
101
|
+
if total > MAX_BUNDLE_SIZE:
|
|
102
|
+
break
|
|
103
|
+
rel = path.relative_to(skill_dir).as_posix()
|
|
104
|
+
try:
|
|
105
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
106
|
+
except OSError:
|
|
107
|
+
continue
|
|
108
|
+
files.append({"path": rel, "content": text})
|
|
109
|
+
return (
|
|
110
|
+
{
|
|
111
|
+
"name": skill_dir.name,
|
|
112
|
+
"description": _read_description(skill_md),
|
|
113
|
+
"content": content,
|
|
114
|
+
"source_path": str(skill_dir),
|
|
115
|
+
"provider": provider,
|
|
116
|
+
"files": files,
|
|
117
|
+
},
|
|
118
|
+
True,
|
|
119
|
+
"",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _find_skill_mds(root: Path, max_depth: int) -> list[Path]:
|
|
124
|
+
found: list[Path] = []
|
|
125
|
+
root_depth = len(root.parts)
|
|
126
|
+
|
|
127
|
+
def walk(d: Path) -> None:
|
|
128
|
+
if len(d.parts) - root_depth > max_depth:
|
|
129
|
+
return
|
|
130
|
+
skill = d / "SKILL.md"
|
|
131
|
+
if skill.is_file():
|
|
132
|
+
found.append(skill)
|
|
133
|
+
return
|
|
134
|
+
try:
|
|
135
|
+
children = list(d.iterdir())
|
|
136
|
+
except OSError:
|
|
137
|
+
return
|
|
138
|
+
for c in children:
|
|
139
|
+
if c.is_dir() and not c.name.startswith("."):
|
|
140
|
+
walk(c)
|
|
141
|
+
|
|
142
|
+
walk(root)
|
|
143
|
+
return found
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _read_description(skill_md: Path) -> str:
|
|
147
|
+
try:
|
|
148
|
+
text = skill_md.read_text(encoding="utf-8", errors="replace")
|
|
149
|
+
except OSError:
|
|
150
|
+
return ""
|
|
151
|
+
for line in text.splitlines():
|
|
152
|
+
line = line.strip()
|
|
153
|
+
if line.startswith("#"):
|
|
154
|
+
continue
|
|
155
|
+
if line:
|
|
156
|
+
return line[:240]
|
|
157
|
+
return ""
|