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/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 ""