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/client.py ADDED
@@ -0,0 +1,438 @@
1
+ """HTTP client for AICortex daemon API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import ssl
7
+ import urllib.error
8
+ import urllib.request
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from .platform_info import host_os
13
+
14
+ MAX_ATTACHMENT_BYTES = 100 << 20
15
+
16
+
17
+ class APIError(Exception):
18
+ def __init__(self, method: str, path: str, status: int, body: str):
19
+ self.method = method
20
+ self.path = path
21
+ self.status = status
22
+ self.body = body
23
+ super().__init__(f"{method} {path} returned {status}: {body[:500]}")
24
+
25
+
26
+ def is_not_found(err: Exception, needle: str) -> bool:
27
+ return (
28
+ isinstance(err, APIError)
29
+ and err.status == 404
30
+ and needle.lower() in err.body.lower()
31
+ )
32
+
33
+
34
+ @dataclass
35
+ class Task:
36
+ raw: dict[str, Any]
37
+
38
+ def _s(self, key: str) -> str:
39
+ return str(self.raw.get(key) or "")
40
+
41
+ @property
42
+ def id(self) -> str:
43
+ return self._s("id")
44
+
45
+ @property
46
+ def runtime_id(self) -> str:
47
+ return self._s("runtime_id")
48
+
49
+ @property
50
+ def issue_id(self) -> str:
51
+ return self._s("issue_id")
52
+
53
+ @property
54
+ def workspace_id(self) -> str:
55
+ return self._s("workspace_id")
56
+
57
+ @property
58
+ def chat_session_id(self) -> str:
59
+ return self._s("chat_session_id")
60
+
61
+ @property
62
+ def chat_message(self) -> str:
63
+ return self._s("chat_message")
64
+
65
+ @property
66
+ def autopilot_run_id(self) -> str:
67
+ return self._s("autopilot_run_id")
68
+
69
+ @property
70
+ def autopilot_id(self) -> str:
71
+ return self._s("autopilot_id")
72
+
73
+ @property
74
+ def autopilot_description(self) -> str:
75
+ return self._s("autopilot_description")
76
+
77
+ @property
78
+ def autopilot_title(self) -> str:
79
+ return self._s("autopilot_title")
80
+
81
+ @property
82
+ def autopilot_source(self) -> str:
83
+ return self._s("autopilot_source")
84
+
85
+ @property
86
+ def autopilot_trigger_payload(self) -> str:
87
+ p = self.raw.get("autopilot_trigger_payload")
88
+ if p is None:
89
+ return ""
90
+ if isinstance(p, (dict, list)):
91
+ return json.dumps(p, ensure_ascii=False)
92
+ return str(p)
93
+
94
+ @property
95
+ def quick_create_prompt(self) -> str:
96
+ return self._s("quick_create_prompt")
97
+
98
+ @property
99
+ def trigger_comment_id(self) -> str:
100
+ return self._s("trigger_comment_id")
101
+
102
+ @property
103
+ def trigger_comment_content(self) -> str:
104
+ return self._s("trigger_comment_content")
105
+
106
+ @property
107
+ def trigger_author_type(self) -> str:
108
+ return self._s("trigger_author_type")
109
+
110
+ @property
111
+ def trigger_author_name(self) -> str:
112
+ return self._s("trigger_author_name")
113
+
114
+ @property
115
+ def project_id(self) -> str:
116
+ return self._s("project_id")
117
+
118
+ @property
119
+ def project_title(self) -> str:
120
+ return self._s("project_title")
121
+
122
+ @property
123
+ def project_prompt(self) -> str:
124
+ return self._s("project_prompt")
125
+
126
+ @property
127
+ def project_pinned_workdir(self) -> bool:
128
+ return bool(self.raw.get("project_pinned_workdir"))
129
+
130
+ @property
131
+ def project_resources(self) -> list[dict[str, Any]]:
132
+ r = self.raw.get("project_resources")
133
+ return [x for x in r if isinstance(x, dict)] if isinstance(r, list) else []
134
+
135
+ @property
136
+ def prior_session_id(self) -> str:
137
+ return self._s("prior_session_id")
138
+
139
+ @property
140
+ def prior_work_dir(self) -> str:
141
+ return self._s("prior_work_dir")
142
+
143
+ @property
144
+ def design_mode(self) -> str:
145
+ return self._s("design_mode")
146
+
147
+ @property
148
+ def design_system_content(self) -> str:
149
+ return self._s("design_system_content")
150
+
151
+ @property
152
+ def design_system_name(self) -> str:
153
+ return self._s("design_system_name")
154
+
155
+ @property
156
+ def design_example_hint(self) -> str:
157
+ return self._s("design_example_hint")
158
+
159
+ @property
160
+ def design_example_id(self) -> str:
161
+ return self._s("design_example_id")
162
+
163
+ @property
164
+ def design_example_bundles(self) -> list[dict[str, Any]]:
165
+ b = self.raw.get("design_example_bundles")
166
+ return [x for x in b if isinstance(x, dict)] if isinstance(b, list) else []
167
+
168
+ @property
169
+ def handoff_bundle_url(self) -> str:
170
+ return self._s("handoff_bundle_url")
171
+
172
+ @property
173
+ def artifact_entry(self) -> str:
174
+ return self._s("artifact_entry")
175
+
176
+ @property
177
+ def session_kind(self) -> str:
178
+ return self._s("session_kind")
179
+
180
+ @property
181
+ def squad_id(self) -> str:
182
+ return self._s("squad_id")
183
+
184
+ @property
185
+ def squad_name(self) -> str:
186
+ return self._s("squad_name")
187
+
188
+ @property
189
+ def agent(self) -> dict[str, Any] | None:
190
+ a = self.raw.get("agent")
191
+ return a if isinstance(a, dict) else None
192
+
193
+ @property
194
+ def repos(self) -> list[dict[str, Any]]:
195
+ repos = self.raw.get("repos")
196
+ if not isinstance(repos, list):
197
+ return []
198
+ return [r for r in repos if isinstance(r, dict)]
199
+
200
+ @property
201
+ def chat_history(self) -> list[dict[str, Any]]:
202
+ h = self.raw.get("chat_history")
203
+ return [x for x in h if isinstance(x, dict)] if isinstance(h, list) else []
204
+
205
+ @property
206
+ def chat_message_attachments(self) -> list[dict[str, Any]]:
207
+ a = self.raw.get("chat_message_attachments")
208
+ if not isinstance(a, list):
209
+ a = self.raw.get("attachments")
210
+ return [x for x in a if isinstance(x, dict)] if isinstance(a, list) else []
211
+
212
+ @property
213
+ def attachments(self) -> list[dict[str, Any]]:
214
+ return self.chat_message_attachments
215
+
216
+ # Daemon-local: filled after materialize
217
+ design_incoming_rel_paths: list[str] | None = None
218
+
219
+
220
+ class DaemonClient:
221
+ def __init__(
222
+ self,
223
+ base_url: str,
224
+ token: str,
225
+ *,
226
+ version: str = "",
227
+ timeout: float = 30.0,
228
+ ):
229
+ self.base_url = base_url.rstrip("/")
230
+ self.token = token
231
+ self.version = version
232
+ self.timeout = timeout
233
+ self._ctx = ssl.create_default_context()
234
+
235
+ def _headers(self, *, json_body: bool = False) -> dict[str, str]:
236
+ headers = {
237
+ "Accept": "application/json",
238
+ "Authorization": f"Bearer {self.token}",
239
+ "X-Client-Platform": "daemon",
240
+ "X-Client-OS": host_os(),
241
+ }
242
+ if self.version:
243
+ headers["X-Client-Version"] = self.version
244
+ if json_body:
245
+ headers["Content-Type"] = "application/json"
246
+ return headers
247
+
248
+ def _request(
249
+ self,
250
+ method: str,
251
+ path: str,
252
+ body: Any | None = None,
253
+ ) -> Any:
254
+ url = self.base_url + path
255
+ data = None
256
+ headers = self._headers(json_body=body is not None)
257
+ if body is not None:
258
+ data = json.dumps(body).encode("utf-8")
259
+
260
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
261
+ try:
262
+ with urllib.request.urlopen(req, timeout=self.timeout, context=self._ctx) as resp:
263
+ raw = resp.read()
264
+ if not raw:
265
+ return None
266
+ return json.loads(raw.decode("utf-8"))
267
+ except urllib.error.HTTPError as e:
268
+ err_body = e.read().decode("utf-8", errors="replace")
269
+ raise APIError(method, path, e.code, err_body) from e
270
+
271
+ def get(self, path: str) -> Any:
272
+ return self._request("GET", path)
273
+
274
+ def post(self, path: str, body: Any | None = None) -> Any:
275
+ return self._request("POST", path, body if body is not None else {})
276
+
277
+ def list_workspaces(self) -> list[dict[str, Any]]:
278
+ data = self.get("/api/workspaces")
279
+ return data if isinstance(data, list) else []
280
+
281
+ def register(self, payload: dict[str, Any]) -> dict[str, Any]:
282
+ data = self.post("/api/daemon/register", payload)
283
+ return data if isinstance(data, dict) else {}
284
+
285
+ def deregister(self, runtime_ids: list[str]) -> None:
286
+ self.post("/api/daemon/deregister", {"runtime_ids": runtime_ids})
287
+
288
+ def recover_orphans(self, runtime_id: str) -> None:
289
+ self.post(f"/api/daemon/runtimes/{runtime_id}/recover-orphans", {})
290
+
291
+ def heartbeat(self, runtime_id: str) -> dict[str, Any]:
292
+ data = self.post("/api/daemon/heartbeat", {"runtime_id": runtime_id})
293
+ return data if isinstance(data, dict) else {}
294
+
295
+ def claim_task(self, runtime_id: str) -> Task | None:
296
+ data = self.post(f"/api/daemon/runtimes/{runtime_id}/tasks/claim", {})
297
+ if not isinstance(data, dict):
298
+ return None
299
+ task = data.get("task")
300
+ if not task:
301
+ return None
302
+ return Task(task)
303
+
304
+ def start_task(self, task_id: str) -> None:
305
+ self.post(f"/api/daemon/tasks/{task_id}/start", {})
306
+
307
+ def report_progress(self, task_id: str, summary: str, step: int, total: int) -> None:
308
+ self.post(
309
+ f"/api/daemon/tasks/{task_id}/progress",
310
+ {"summary": summary, "step": step, "total": total},
311
+ )
312
+
313
+ def complete_task(
314
+ self,
315
+ task_id: str,
316
+ output: str,
317
+ *,
318
+ work_dir: str = "",
319
+ session_id: str = "",
320
+ branch_name: str = "",
321
+ ) -> None:
322
+ body: dict[str, Any] = {"output": output}
323
+ if work_dir:
324
+ body["work_dir"] = work_dir
325
+ if session_id:
326
+ body["session_id"] = session_id
327
+ if branch_name:
328
+ body["branch_name"] = branch_name
329
+ self.post(f"/api/daemon/tasks/{task_id}/complete", body)
330
+
331
+ def fail_task(
332
+ self,
333
+ task_id: str,
334
+ error: str,
335
+ *,
336
+ work_dir: str = "",
337
+ session_id: str = "",
338
+ failure_reason: str = "agent_error",
339
+ ) -> None:
340
+ body: dict[str, Any] = {"error": error, "failure_reason": failure_reason}
341
+ if work_dir:
342
+ body["work_dir"] = work_dir
343
+ if session_id:
344
+ body["session_id"] = session_id
345
+ self.post(f"/api/daemon/tasks/{task_id}/fail", body)
346
+
347
+ def pin_task_session(
348
+ self,
349
+ task_id: str,
350
+ *,
351
+ session_id: str = "",
352
+ work_dir: str = "",
353
+ ) -> None:
354
+ if not session_id and not work_dir:
355
+ return
356
+ body: dict[str, Any] = {}
357
+ if session_id:
358
+ body["session_id"] = session_id
359
+ if work_dir:
360
+ body["work_dir"] = work_dir
361
+ self.post(f"/api/daemon/tasks/{task_id}/session", body)
362
+
363
+ def get_task_status(self, task_id: str) -> str:
364
+ data = self.get(f"/api/daemon/tasks/{task_id}/status")
365
+ if isinstance(data, dict):
366
+ return str(data.get("status") or "")
367
+ return ""
368
+
369
+ def report_task_messages(self, task_id: str, messages: list[dict[str, Any]]) -> None:
370
+ if not messages:
371
+ return
372
+ self.post(f"/api/daemon/tasks/{task_id}/messages", {"messages": messages})
373
+
374
+ def report_task_usage(self, task_id: str, usage: list[dict[str, Any]]) -> None:
375
+ if not usage:
376
+ return
377
+ self.post(f"/api/daemon/tasks/{task_id}/usage", {"usage": usage})
378
+
379
+ def report_task_artifacts(self, task_id: str, artifacts: list[dict[str, str]]) -> None:
380
+ if not artifacts:
381
+ return
382
+ self.post(f"/api/daemon/tasks/{task_id}/artifacts", {"artifacts": artifacts})
383
+
384
+ def report_model_list_result(
385
+ self, runtime_id: str, request_id: str, result: dict[str, Any]
386
+ ) -> None:
387
+ self.post(f"/api/daemon/runtimes/{runtime_id}/models/{request_id}/result", result)
388
+
389
+ def report_local_skill_list_result(
390
+ self, runtime_id: str, request_id: str, result: dict[str, Any]
391
+ ) -> None:
392
+ self.post(
393
+ f"/api/daemon/runtimes/{runtime_id}/local-skills/{request_id}/result",
394
+ result,
395
+ )
396
+
397
+ def report_local_skill_import_result(
398
+ self, runtime_id: str, request_id: str, result: dict[str, Any]
399
+ ) -> None:
400
+ self.post(
401
+ f"/api/daemon/runtimes/{runtime_id}/local-skills/import/{request_id}/result",
402
+ result,
403
+ )
404
+
405
+ def get_issue_gc_check(self, issue_id: str) -> dict[str, Any]:
406
+ data = self.get(f"/api/daemon/issues/{issue_id}/gc-check")
407
+ return data if isinstance(data, dict) else {}
408
+
409
+ def get_chat_session_gc_check(self, session_id: str) -> dict[str, Any]:
410
+ data = self.get(f"/api/daemon/chat-sessions/{session_id}/gc-check")
411
+ return data if isinstance(data, dict) else {}
412
+
413
+ def get_autopilot_run_gc_check(self, run_id: str) -> dict[str, Any]:
414
+ data = self.get(f"/api/daemon/autopilot-runs/{run_id}/gc-check")
415
+ return data if isinstance(data, dict) else {}
416
+
417
+ def get_task_gc_check(self, task_id: str) -> dict[str, Any]:
418
+ data = self.get(f"/api/daemon/tasks/{task_id}/gc-check")
419
+ return data if isinstance(data, dict) else {}
420
+
421
+ def get_workspace_repos(self, workspace_id: str) -> dict[str, Any]:
422
+ data = self.get(f"/api/daemon/workspaces/{workspace_id}/repos")
423
+ return data if isinstance(data, dict) else {}
424
+
425
+ def fetch_attachment_bytes(self, attachment_id: str) -> tuple[bytes, str]:
426
+ path = f"/api/daemon/attachments/{attachment_id}/bytes"
427
+ url = self.base_url + path
428
+ req = urllib.request.Request(url, headers=self._headers(), method="GET")
429
+ try:
430
+ with urllib.request.urlopen(req, timeout=self.timeout, context=self._ctx) as resp:
431
+ body = resp.read(MAX_ATTACHMENT_BYTES + 1)
432
+ if len(body) > MAX_ATTACHMENT_BYTES:
433
+ raise APIError("GET", path, 413, "attachment exceeds size limit")
434
+ filename = resp.headers.get("X-Attachment-Filename") or ""
435
+ return body, filename
436
+ except urllib.error.HTTPError as e:
437
+ err_body = e.read().decode("utf-8", errors="replace")
438
+ raise APIError("GET", path, e.code, err_body) from e
@@ -0,0 +1,98 @@
1
+ """Codex per-task CODEX_HOME config helpers (skill strip + multi-agent disable)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+
10
+ log = logging.getLogger("aicortexpy.codex_config")
11
+
12
+ MULTI_AGENT_BEGIN = "# BEGIN aicortex-managed multi-agent (do not edit; regenerated by daemon)"
13
+ MULTI_AGENT_END = "# END aicortex-managed multi-agent"
14
+ _MULTI_AGENT_BLOCK_RE = re.compile(
15
+ rf"(?ms)^{re.escape(MULTI_AGENT_BEGIN)}.*?^{re.escape(MULTI_AGENT_END)}\n*"
16
+ )
17
+
18
+
19
+ def strip_skills_config_entries(content: str) -> str:
20
+ """Remove [[skills.config]] blocks that break Codex CLI (missing path)."""
21
+ if "[[skills.config]]" not in content:
22
+ return content
23
+ out: list[str] = []
24
+ in_skills = False
25
+ for line in content.splitlines(keepends=True):
26
+ trimmed = line.strip()
27
+ if trimmed.startswith("["):
28
+ if trimmed == "[[skills.config]]":
29
+ in_skills = True
30
+ continue
31
+ in_skills = False
32
+ out.append(line)
33
+ continue
34
+ if in_skills:
35
+ continue
36
+ out.append(line)
37
+ stripped = "".join(out).rstrip("\n") + "\n"
38
+ return "" if not stripped.strip() else stripped
39
+
40
+
41
+ def sanitize_copied_codex_config(config_path: Path) -> None:
42
+ if not config_path.is_file():
43
+ return
44
+ try:
45
+ text = config_path.read_text(encoding="utf-8")
46
+ except OSError as e:
47
+ log.warning("read codex config failed: %s", e)
48
+ return
49
+ stripped = strip_skills_config_entries(text)
50
+ if stripped == text:
51
+ return
52
+ try:
53
+ config_path.write_text(stripped, encoding="utf-8")
54
+ log.info("stripped [[skills.config]] from %s", config_path)
55
+ except OSError as e:
56
+ log.warning("write sanitized codex config failed: %s", e)
57
+
58
+
59
+ def codex_multi_agent_enabled() -> bool:
60
+ raw = (os.environ.get("AICORTEX_CODEX_MULTI_AGENT") or "").strip().lower()
61
+ return raw in ("1", "true", "yes", "on")
62
+
63
+
64
+ def ensure_codex_multi_agent_config(config_path: Path) -> None:
65
+ """Disable Codex native multi_agent unless AICORTEX_CODEX_MULTI_AGENT is set."""
66
+ if codex_multi_agent_enabled():
67
+ return
68
+ config_path.parent.mkdir(parents=True, exist_ok=True)
69
+ existing = ""
70
+ if config_path.is_file():
71
+ try:
72
+ existing = config_path.read_text(encoding="utf-8")
73
+ except OSError:
74
+ existing = ""
75
+ existing = _MULTI_AGENT_BLOCK_RE.sub("", existing)
76
+ in_features = bool(re.search(r"(?m)^\s*\[\s*features\s*\]\s*(?:#.*)?$", existing))
77
+ if in_features:
78
+ body = "multi_agent = false\n"
79
+ else:
80
+ body = "features.multi_agent = false\n"
81
+ block = f"{MULTI_AGENT_BEGIN}\n{body}{MULTI_AGENT_END}\n"
82
+ # Prefer injecting inside [features] when present.
83
+ if in_features:
84
+ lines = existing.splitlines(keepends=True)
85
+ out: list[str] = []
86
+ injected = False
87
+ for i, line in enumerate(lines):
88
+ out.append(line)
89
+ if not injected and re.match(r"^\s*\[\s*features\s*\]\s*(?:#.*)?$", line.strip()):
90
+ out.append(block)
91
+ injected = True
92
+ text = "".join(out) if injected else block + existing
93
+ else:
94
+ text = block + (existing if existing.endswith("\n") or not existing else existing + "\n")
95
+ try:
96
+ config_path.write_text(text, encoding="utf-8")
97
+ except OSError as e:
98
+ log.warning("write multi-agent config failed: %s", e)