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/process.py ADDED
@@ -0,0 +1,168 @@
1
+ """Background process helpers for Windows / POSIX."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ from pathlib import Path
10
+
11
+ from .config import Config
12
+ from .health import fetch_health, request_shutdown
13
+
14
+
15
+ def pid_path(cfg: Config) -> Path:
16
+ return cfg.state_dir / "python-daemon.pid"
17
+
18
+
19
+ def log_path(cfg: Config) -> Path:
20
+ return cfg.state_dir / "python-daemon.log"
21
+
22
+
23
+ def write_pid(cfg: Config, pid: int) -> None:
24
+ cfg.state_dir.mkdir(parents=True, exist_ok=True)
25
+ pid_path(cfg).write_text(str(pid), encoding="utf-8")
26
+
27
+
28
+ def read_pid(cfg: Config) -> int | None:
29
+ p = pid_path(cfg)
30
+ if not p.is_file():
31
+ return None
32
+ try:
33
+ return int(p.read_text(encoding="utf-8").strip())
34
+ except ValueError:
35
+ return None
36
+
37
+
38
+ def clear_pid(cfg: Config) -> None:
39
+ try:
40
+ pid_path(cfg).unlink(missing_ok=True)
41
+ except OSError:
42
+ pass
43
+
44
+
45
+ def pid_alive(pid: int) -> bool:
46
+ if pid <= 0:
47
+ return False
48
+ if sys.platform == "win32":
49
+ import ctypes
50
+
51
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
52
+ handle = ctypes.windll.kernel32.OpenProcess(
53
+ PROCESS_QUERY_LIMITED_INFORMATION, False, pid
54
+ )
55
+ if not handle:
56
+ return False
57
+ ctypes.windll.kernel32.CloseHandle(handle)
58
+ return True
59
+ try:
60
+ os.kill(pid, 0)
61
+ return True
62
+ except OSError:
63
+ return False
64
+
65
+
66
+ def spawn_background(cfg: Config, extra_args: list[str] | None = None) -> int:
67
+ """Start `python -m aicortexpy run` detached; return child PID."""
68
+ cfg.state_dir.mkdir(parents=True, exist_ok=True)
69
+ log_file = log_path(cfg)
70
+ args = [
71
+ sys.executable,
72
+ "-m",
73
+ "aicortexpy",
74
+ "run",
75
+ ]
76
+ if cfg.profile:
77
+ args.extend(["--profile", cfg.profile])
78
+ if extra_args:
79
+ args.extend(extra_args)
80
+
81
+ log_fh = open(log_file, "a", encoding="utf-8")
82
+ popen_kwargs: dict = {
83
+ "stdin": subprocess.DEVNULL,
84
+ "stdout": log_fh,
85
+ "stderr": subprocess.STDOUT,
86
+ "cwd": str(Path.cwd()),
87
+ "close_fds": True,
88
+ }
89
+
90
+ if sys.platform == "win32":
91
+ # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB
92
+ CREATE_BREAKAWAY_FROM_JOB = 0x01000000
93
+ flags = (
94
+ subprocess.DETACHED_PROCESS
95
+ | subprocess.CREATE_NEW_PROCESS_GROUP
96
+ | CREATE_BREAKAWAY_FROM_JOB
97
+ )
98
+ try:
99
+ proc = subprocess.Popen(args, creationflags=flags, **popen_kwargs)
100
+ except OSError:
101
+ # Parent job may disallow breakaway — fall back.
102
+ flags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
103
+ proc = subprocess.Popen(args, creationflags=flags, **popen_kwargs)
104
+ else:
105
+ popen_kwargs["start_new_session"] = True
106
+ proc = subprocess.Popen(args, **popen_kwargs)
107
+
108
+ write_pid(cfg, proc.pid)
109
+ # Give health server a moment to bind
110
+ for _ in range(20):
111
+ time.sleep(0.25)
112
+ if fetch_health(cfg.health_port):
113
+ break
114
+ return proc.pid
115
+
116
+
117
+ def stop_daemon(cfg: Config) -> str:
118
+ health = fetch_health(cfg.health_port)
119
+ if health:
120
+ if request_shutdown(cfg.health_port):
121
+ for _ in range(40):
122
+ time.sleep(0.25)
123
+ if not fetch_health(cfg.health_port):
124
+ clear_pid(cfg)
125
+ return "stopped via /shutdown"
126
+ return "shutdown requested but process still responding"
127
+
128
+ pid = read_pid(cfg)
129
+ if pid and pid_alive(pid):
130
+ if sys.platform == "win32":
131
+ subprocess.run(
132
+ ["taskkill", "/PID", str(pid), "/T", "/F"],
133
+ capture_output=True,
134
+ check=False,
135
+ )
136
+ else:
137
+ os.kill(pid, 15)
138
+ time.sleep(1)
139
+ if pid_alive(pid):
140
+ os.kill(pid, 9)
141
+ clear_pid(cfg)
142
+ return f"killed pid {pid}"
143
+
144
+ clear_pid(cfg)
145
+ return "daemon not running"
146
+
147
+
148
+ def status_text(cfg: Config) -> str:
149
+ health = fetch_health(cfg.health_port)
150
+ if health:
151
+ lines = [
152
+ "status: running",
153
+ f"pid: {health.get('pid')}",
154
+ f"uptime: {health.get('uptime')}",
155
+ f"daemon_id: {health.get('daemon_id')}",
156
+ f"server: {health.get('server_url')}",
157
+ f"version: {health.get('cli_version')}",
158
+ f"active_tasks: {health.get('active_task_count')}",
159
+ f"agents: {', '.join(health.get('agents') or [])}",
160
+ f"workspaces: {len(health.get('workspaces') or [])}",
161
+ f"health: http://127.0.0.1:{cfg.health_port}/health",
162
+ ]
163
+ return "\n".join(lines)
164
+
165
+ pid = read_pid(cfg)
166
+ if pid and pid_alive(pid):
167
+ return f"status: pid {pid} alive but health port {cfg.health_port} not responding"
168
+ return "status: not running"
aicortexpy/procutil.py ADDED
@@ -0,0 +1,61 @@
1
+ """Cross-platform process-tree helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import signal
8
+ import subprocess
9
+ import sys
10
+ import time
11
+
12
+ log = logging.getLogger("aicortexpy.procutil")
13
+
14
+
15
+ def popen_kwargs_for_killable_group() -> dict:
16
+ """Flags so the child (and its tree) can be killed as a unit."""
17
+ if sys.platform == "win32":
18
+ return {
19
+ "creationflags": subprocess.CREATE_NEW_PROCESS_GROUP
20
+ | getattr(subprocess, "CREATE_NO_WINDOW", 0),
21
+ }
22
+ return {"start_new_session": True}
23
+
24
+
25
+ def kill_process_tree(pid: int, *, graceful_seconds: float = 2.0) -> None:
26
+ if pid <= 0:
27
+ return
28
+ if sys.platform == "win32":
29
+ subprocess.run(
30
+ ["taskkill", "/PID", str(pid), "/T", "/F"],
31
+ capture_output=True,
32
+ check=False,
33
+ )
34
+ return
35
+
36
+ try:
37
+ os.killpg(pid, signal.SIGTERM)
38
+ except ProcessLookupError:
39
+ return
40
+ except PermissionError:
41
+ try:
42
+ os.kill(pid, signal.SIGTERM)
43
+ except ProcessLookupError:
44
+ return
45
+
46
+ deadline = time.time() + graceful_seconds
47
+ while time.time() < deadline:
48
+ try:
49
+ os.kill(pid, 0)
50
+ except ProcessLookupError:
51
+ return
52
+ time.sleep(0.1)
53
+
54
+ try:
55
+ os.killpg(pid, signal.SIGKILL)
56
+ except (ProcessLookupError, PermissionError):
57
+ try:
58
+ os.kill(pid, signal.SIGKILL)
59
+ except ProcessLookupError:
60
+ pass
61
+ log.info("killed process tree pid=%s", pid)
aicortexpy/prompt.py ADDED
@@ -0,0 +1,235 @@
1
+ """Task prompt builders (ported from Go daemon prompt.go)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .client import Task
6
+ from .runtime_config import build_comment_reply_instructions
7
+
8
+
9
+ def build_prompt(task: Task, provider: str = "") -> str:
10
+ if task.design_mode:
11
+ return build_design_prompt(task)
12
+ if task.chat_session_id:
13
+ return build_chat_prompt(task)
14
+ if task.trigger_comment_id or task.trigger_comment_content:
15
+ return build_comment_prompt(task, provider)
16
+ if task.autopilot_run_id:
17
+ return build_autopilot_prompt(task)
18
+ if task.quick_create_prompt:
19
+ return build_quick_create_prompt(task)
20
+ return build_issue_prompt(task)
21
+
22
+
23
+ def build_issue_prompt(task: Task) -> str:
24
+ parts = [
25
+ "You are running as a local coding agent for a AICortex workspace.\n\n",
26
+ f"Your assigned issue ID is: {task.issue_id}\n\n",
27
+ ]
28
+ if task.project_title or task.project_prompt:
29
+ if task.project_title:
30
+ parts.append(f"Project: {task.project_title}\n")
31
+ if task.project_prompt:
32
+ parts.append(f"Project instructions:\n{task.project_prompt}\n\n")
33
+ parts.append(
34
+ f"Start by running `aicortex issue get {task.issue_id} --output json` "
35
+ "to understand your task, then complete it.\n"
36
+ f"If you need comment history, `aicortex issue comment list {task.issue_id} "
37
+ "--output json` returns all comments for the issue (server caps at 2000). "
38
+ "Pass `--since <RFC3339>` to fetch only comments newer than a known cursor.\n"
39
+ )
40
+ return "".join(parts)
41
+
42
+
43
+ def build_comment_prompt(task: Task, provider: str) -> str:
44
+ parts = [
45
+ "You are running as a local coding agent for a AICortex workspace.\n\n",
46
+ f"Your assigned issue ID is: {task.issue_id}\n\n",
47
+ ]
48
+ if task.trigger_comment_content:
49
+ author = "A user"
50
+ if task.trigger_author_type == "agent":
51
+ name = task.trigger_author_name or "another agent"
52
+ author = f"Another agent ({name})"
53
+ parts.append(
54
+ f"[NEW COMMENT] {author} just left a new comment. "
55
+ "Focus on THIS comment — do not confuse it with previous ones:\n\n"
56
+ f"> {task.trigger_comment_content}\n\n"
57
+ )
58
+ if task.trigger_author_type == "agent":
59
+ parts.append(
60
+ "The triggering comment was posted by another agent. "
61
+ "If you produced real work this turn, post the result as a normal reply. "
62
+ "If it was only acknowledgment/thanks and you produced no work, do NOT reply "
63
+ "and do NOT @mention the other agent (that re-triggers a loop).\n\n"
64
+ )
65
+ parts.append(
66
+ f"Start by running `aicortex issue get {task.issue_id} --output json` "
67
+ "to understand your task, then decide how to proceed.\n\n"
68
+ f"If you need comment history, `aicortex issue comment list {task.issue_id} "
69
+ "--output json`.\n\n"
70
+ )
71
+ parent = task.trigger_comment_id
72
+ if parent:
73
+ parts.append(build_comment_reply_instructions(provider, task.issue_id, parent))
74
+ else:
75
+ parts.append(
76
+ f"Final results MUST be delivered via `aicortex issue comment add {task.issue_id}`.\n"
77
+ )
78
+ return "".join(parts)
79
+
80
+
81
+ def build_chat_prompt(task: Task) -> str:
82
+ parts = [
83
+ "You are running as a chat assistant for a AICortex workspace.\n",
84
+ "A user is chatting with you directly. Respond to their message.\n\n",
85
+ ]
86
+ if task.chat_history:
87
+ parts.append("Recent conversation (context only; answer the latest user message below):\n")
88
+ for m in task.chat_history:
89
+ role = str(m.get("role") or "unknown").strip() or "unknown"
90
+ content = str(m.get("content") or "")
91
+ parts.append(f"{role}:\n{content}\n\n")
92
+ parts.append(f"User message:\n{task.chat_message}\n")
93
+ atts = task.chat_message_attachments
94
+ if atts:
95
+ parts.append("\nAttachments on this message:\n")
96
+ for a in atts:
97
+ aid = a.get("id")
98
+ fn = a.get("filename")
99
+ ct = a.get("content_type") or ""
100
+ if ct:
101
+ parts.append(f'- id={aid} filename="{fn}" content_type={ct}\n')
102
+ else:
103
+ parts.append(f'- id={aid} filename="{fn}"\n')
104
+ incoming = task.design_incoming_rel_paths or []
105
+ if incoming:
106
+ parts.append("\nThese files are already copied into the work directory:\n")
107
+ for rel in incoming:
108
+ parts.append(f"- `{rel}`\n")
109
+ parts.append(
110
+ "Read them directly from the work directory. "
111
+ "Use `aicortex attachment download <id>` only if you need another copy.\n"
112
+ )
113
+ else:
114
+ parts.append(
115
+ "Use `aicortex attachment download <id>` to fetch each file locally "
116
+ "before referring to it.\n"
117
+ )
118
+ return "".join(parts)
119
+
120
+
121
+ def build_autopilot_prompt(task: Task) -> str:
122
+ parts = [
123
+ "You are running as a local coding agent for a AICortex workspace.\n\n",
124
+ "This task was triggered by an Autopilot in run-only mode. "
125
+ "There is no assigned AICortex issue for this run.\n\n",
126
+ f"Autopilot run ID: {task.autopilot_run_id}\n",
127
+ ]
128
+ if task.autopilot_id:
129
+ parts.append(f"Autopilot ID: {task.autopilot_id}\n")
130
+ if task.autopilot_title:
131
+ parts.append(f"Autopilot title: {task.autopilot_title}\n")
132
+ if task.autopilot_source:
133
+ parts.append(f"Trigger source: {task.autopilot_source}\n")
134
+ if task.autopilot_trigger_payload.strip():
135
+ parts.append(f"Trigger payload:\n{task.autopilot_trigger_payload.strip()}\n")
136
+ parts.append("\nAutopilot instructions:\n")
137
+ if task.autopilot_description.strip():
138
+ parts.append(task.autopilot_description + "\n\n")
139
+ elif task.autopilot_title:
140
+ parts.append(task.autopilot_title + "\n\n")
141
+ else:
142
+ parts.append(
143
+ "No additional autopilot instructions were provided. "
144
+ "Inspect the autopilot configuration before proceeding.\n\n"
145
+ )
146
+ if task.autopilot_id:
147
+ parts.append(
148
+ f"Start by running `aicortex autopilot get {task.autopilot_id} --output json` "
149
+ "if you need the full autopilot configuration, then complete the instructions above.\n"
150
+ )
151
+ else:
152
+ parts.append("Complete the instructions above.\n")
153
+ parts.append("Do not run `aicortex issue get`; this run does not have an issue ID.\n")
154
+ return "".join(parts)
155
+
156
+
157
+ def build_quick_create_prompt(task: Task) -> str:
158
+ parts = [
159
+ "You are running as a quick-create assistant for a AICortex workspace.\n\n",
160
+ "A user captured the following input via the quick-create modal. "
161
+ "There is NO existing issue. Your job is to create a well-formed issue "
162
+ "from this input with a single `aicortex issue create` command.\n\n",
163
+ f"User input:\n> {task.quick_create_prompt}\n\n",
164
+ "Field rules:\n\n",
165
+ "- **title**: required. Concise but semantically rich summary.\n\n",
166
+ "- **description**: High fidelity restatement of user intent "
167
+ "(User request + optional Context sections).\n\n",
168
+ "- **priority**: urgent|high|medium|low or omit.\n\n",
169
+ "- **assignee**: resolve names via workspace members/agents/squads JSON; "
170
+ "prefer `--assignee-id <uuid>`.\n",
171
+ ]
172
+ agent = task.agent or {}
173
+ agent_id = str(agent.get("id") or "")
174
+ agent_name = str(agent.get("name") or "")
175
+ if task.squad_id:
176
+ label = task.squad_name or task.squad_id
177
+ parts.append(
178
+ f' - When the user did NOT name an assignee, default to the picker SQUAD "{label}": '
179
+ f'pass `--assignee-id "{task.squad_id}"`.\n\n'
180
+ )
181
+ elif agent_id:
182
+ parts.append(
183
+ f' - When the user did NOT name an assignee, default to YOURSELF: '
184
+ f'pass `--assignee-id "{agent_id}"`.\n\n'
185
+ )
186
+ elif agent_name:
187
+ parts.append(
188
+ f' - When the user did NOT name an assignee, default to YOURSELF: '
189
+ f'pass `--assignee "{agent_name}"`.\n\n'
190
+ )
191
+ if task.project_id:
192
+ title = task.project_title or task.project_id
193
+ parts.append(
194
+ f'- **project**: required. Pass `--project "{task.project_id}"` '
195
+ f'(project "{title}").\n'
196
+ )
197
+ else:
198
+ parts.append("- **project**: omit.\n")
199
+ parts.append(
200
+ "- **status**: omit (defaults to todo).\n"
201
+ "- **attachments**: do NOT pass `--attachment` for remote URLs.\n\n"
202
+ "Output format:\n"
203
+ "- Run exactly one `aicortex issue create --output json`.\n"
204
+ "- Print exactly one line: `Created <identifier-or-id>: <title>` and exit.\n"
205
+ "- Do NOT call `aicortex issue get` or comment add.\n"
206
+ )
207
+ return "".join(parts)
208
+
209
+
210
+ def build_design_prompt(task: Task) -> str:
211
+ parts = [
212
+ "You are running in AICortex Design Studio mode.\n\n",
213
+ f"Design mode: {task.design_mode}\n",
214
+ ]
215
+ if task.design_system_name:
216
+ parts.append(f"Design system: {task.design_system_name}\n")
217
+ if task.design_system_content:
218
+ parts.append(f"\nDesign system content:\n{task.design_system_content}\n")
219
+ if task.design_example_hint:
220
+ parts.append(f"\nExample hint: {task.design_example_hint}\n")
221
+ if task.artifact_entry:
222
+ parts.append(f"\nPrimary artifact entry: {task.artifact_entry}\n")
223
+ if task.project_prompt:
224
+ parts.append(f"\nProject instructions:\n{task.project_prompt}\n")
225
+ parts.append(f"\nUser message:\n{task.chat_message}\n")
226
+ incoming = task.design_incoming_rel_paths or []
227
+ if incoming:
228
+ parts.append("\nIncoming files already in the work directory:\n")
229
+ for rel in incoming:
230
+ parts.append(f"- `{rel}`\n")
231
+ parts.append(
232
+ "\nProduce the design artifacts in the work directory. "
233
+ "Prefer HTML under design/ when applicable.\n"
234
+ )
235
+ return "".join(parts)