aicortexpy 0.1.0__tar.gz → 0.2.1__tar.gz

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 (96) hide show
  1. aicortexpy-0.2.1/PKG-INFO +87 -0
  2. aicortexpy-0.2.1/README.md +70 -0
  3. aicortexpy-0.2.1/aicortexpy/__init__.py +3 -0
  4. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/acp_client.py +20 -3
  5. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/acp_conn.py +2 -2
  6. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/agent_env.py +49 -6
  7. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/attachments.py +3 -1
  8. aicortexpy-0.2.1/aicortexpy/cli.py +177 -0
  9. aicortexpy-0.2.1/aicortexpy/cli_context.py +99 -0
  10. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/client.py +9 -2
  11. aicortexpy-0.2.1/aicortexpy/commands/__init__.py +1 -0
  12. aicortexpy-0.2.1/aicortexpy/commands/agent_cmd.py +146 -0
  13. aicortexpy-0.2.1/aicortexpy/commands/attachment_cmd.py +45 -0
  14. aicortexpy-0.2.1/aicortexpy/commands/auth_cmd.py +167 -0
  15. aicortexpy-0.2.1/aicortexpy/commands/autopilot_cmd.py +184 -0
  16. aicortexpy-0.2.1/aicortexpy/commands/config_cmd.py +50 -0
  17. aicortexpy-0.2.1/aicortexpy/commands/daemon_cmd.py +228 -0
  18. aicortexpy-0.2.1/aicortexpy/commands/graph_cmd.py +69 -0
  19. aicortexpy-0.2.1/aicortexpy/commands/issue_cmd.py +436 -0
  20. aicortexpy-0.2.1/aicortexpy/commands/label_cmd.py +68 -0
  21. aicortexpy-0.2.1/aicortexpy/commands/meeting_cmd.py +125 -0
  22. aicortexpy-0.2.1/aicortexpy/commands/memory_cmd.py +45 -0
  23. aicortexpy-0.2.1/aicortexpy/commands/project_cmd.py +97 -0
  24. aicortexpy-0.2.1/aicortexpy/commands/repo_cmd.py +48 -0
  25. aicortexpy-0.2.1/aicortexpy/commands/runtime_cmd.py +48 -0
  26. aicortexpy-0.2.1/aicortexpy/commands/setup_cmd.py +159 -0
  27. aicortexpy-0.2.1/aicortexpy/commands/skill_cmd.py +121 -0
  28. aicortexpy-0.2.1/aicortexpy/commands/squad_cmd.py +114 -0
  29. aicortexpy-0.2.1/aicortexpy/commands/update_cmd.py +46 -0
  30. aicortexpy-0.2.1/aicortexpy/commands/version_cmd.py +17 -0
  31. aicortexpy-0.2.1/aicortexpy/commands/workspace_cmd.py +62 -0
  32. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/config.py +82 -8
  33. aicortexpy-0.2.1/aicortexpy/id_resolve.py +233 -0
  34. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/models.py +8 -0
  35. aicortexpy-0.2.1/aicortexpy/oauth_login.py +183 -0
  36. aicortexpy-0.2.1/aicortexpy/output.py +77 -0
  37. aicortexpy-0.2.1/aicortexpy/platform_client.py +210 -0
  38. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/process.py +2 -1
  39. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/runner.py +50 -2
  40. aicortexpy-0.2.1/aicortexpy.egg-info/PKG-INFO +87 -0
  41. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/SOURCES.txt +27 -0
  42. aicortexpy-0.2.1/aicortexpy.egg-info/entry_points.txt +3 -0
  43. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/pyproject.toml +7 -7
  44. aicortexpy-0.1.0/PKG-INFO +0 -106
  45. aicortexpy-0.1.0/README.md +0 -89
  46. aicortexpy-0.1.0/aicortexpy/__init__.py +0 -3
  47. aicortexpy-0.1.0/aicortexpy/cli.py +0 -133
  48. aicortexpy-0.1.0/aicortexpy.egg-info/PKG-INFO +0 -106
  49. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/MANIFEST.in +0 -0
  50. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/__main__.py +0 -0
  51. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/acp_errors.py +0 -0
  52. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/artifact_ws.py +0 -0
  53. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/artifacts.py +0 -0
  54. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/auto_commit.py +0 -0
  55. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/capabilities.py +0 -0
  56. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/chat_acp_pool.py +0 -0
  57. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/codex_config.py +0 -0
  58. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/daemon.py +0 -0
  59. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
  60. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
  61. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
  62. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
  63. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
  64. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
  65. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
  66. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
  67. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +0 -0
  68. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
  69. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
  70. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
  71. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
  72. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
  73. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/design_seed.py +0 -0
  74. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/diskusage.py +0 -0
  75. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/execenv.py +0 -0
  76. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/gc.py +0 -0
  77. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/handoff_bundle.py +0 -0
  78. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/health.py +0 -0
  79. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/local_skills.py +0 -0
  80. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/openclaw_config.py +0 -0
  81. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/pinned_workdir.py +0 -0
  82. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/platform_info.py +0 -0
  83. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/poisoned.py +0 -0
  84. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/procutil.py +0 -0
  85. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/prompt.py +0 -0
  86. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/repo.py +0 -0
  87. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/runtime_config.py +0 -0
  88. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/sandbox.py +0 -0
  89. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/stream.py +0 -0
  90. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/terminal.py +0 -0
  91. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/tunnel.py +0 -0
  92. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy/wakeup.py +0 -0
  93. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/dependency_links.txt +0 -0
  94. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/requires.txt +0 -0
  95. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/top_level.txt +0 -0
  96. {aicortexpy-0.1.0 → aicortexpy-0.2.1}/setup.cfg +0 -0
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.2
2
+ Name: aicortexpy
3
+ Version: 0.2.1
4
+ Summary: AICortex Python CLI and local agent runtime daemon (Go CLI compatible)
5
+ Author: AICortex
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://gitee.com/seacozz2007/aicortex
8
+ Project-URL: Repository, https://gitee.com/seacozz2007/aicortex
9
+ Project-URL: Issues, https://gitee.com/seacozz2007/aicortex/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: websocket-client>=1.6
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.0; extra == "dev"
15
+ Requires-Dist: build>=1.0; extra == "dev"
16
+ Requires-Dist: twine>=5.0; extra == "dev"
17
+
18
+ # aicortexpy
19
+
20
+ Python **CLI + local agent runtime** for AICortex。在无法使用 Go `aicortex` 二进制时,可作为完整替代:
21
+
22
+ ```powershell
23
+ pip install -U aicortexpy
24
+ aicortex login --token mul_...
25
+ aicortex daemon start
26
+ aicortex issue list --output json
27
+ ```
28
+
29
+ 也可用 `python -m aicortexpy`(与 `aicortex` / `aicortexpy` 入口等价)。
30
+
31
+ | | |
32
+ |--|--|
33
+ | PyPI / 模块 | `aicortexpy` |
34
+ | Console scripts | `aicortex`、`aicortexpy`(`aicortex` 刻意同名,供 Agent 提示词零改动) |
35
+ | 配置 | 与 Go 共用 `~/.aicortex/config.json` |
36
+
37
+ ## 安装
38
+
39
+ ```powershell
40
+ pip install -U aicortexpy
41
+ # 源码:
42
+ cd python-daemon
43
+ pip install -e .
44
+ ```
45
+
46
+ ## 常用命令(对齐 Go CLI)
47
+
48
+ ```powershell
49
+ aicortex login --token mul_xxx
50
+ aicortex auth status
51
+ aicortex config show
52
+ aicortex workspace list --output json
53
+ aicortex issue get MUL-123 --output json
54
+ aicortex issue comment add MUL-123 --content "done"
55
+ aicortex daemon start
56
+ aicortex daemon status
57
+ aicortex version
58
+ ```
59
+
60
+ Agent 任务内:daemon 会把 `aicortex` shim 前置到 PATH,子进程执行 `aicortex issue ...` 即本包,无需 Go 二进制。
61
+
62
+ ## Daemon
63
+
64
+ ```powershell
65
+ $env:AICORTEX_DAEMON_BACKEND = "python" # 若仍用 Go 包装器拉起
66
+ aicortex daemon start
67
+ # 或
68
+ python -m aicortexpy daemon start --foreground
69
+ ```
70
+
71
+ Health 默认端口 **19514**。不要与 Go daemon 同时跑。
72
+
73
+ ## 发布
74
+
75
+ ```powershell
76
+ cd python-daemon
77
+ python -m build
78
+ python -m twine upload dist/*
79
+ ```
80
+
81
+ 版本:`pyproject.toml` 与 `aicortexpy/__init__.py` 的 `__version__` 保持一致。
82
+
83
+ ## 相对 Go CLI
84
+
85
+ 已覆盖 Agent/日常平台命令(issue/workspace/agent/squad/project/label/attachment/autopilot/meeting/skill/memory/graph/runtime/repo/auth/config/daemon)。
86
+
87
+ 尚未:浏览器 OAuth login、`update` 二进制自更新(请用 `pip install -U aicortexpy`)。
@@ -0,0 +1,70 @@
1
+ # aicortexpy
2
+
3
+ Python **CLI + local agent runtime** for AICortex。在无法使用 Go `aicortex` 二进制时,可作为完整替代:
4
+
5
+ ```powershell
6
+ pip install -U aicortexpy
7
+ aicortex login --token mul_...
8
+ aicortex daemon start
9
+ aicortex issue list --output json
10
+ ```
11
+
12
+ 也可用 `python -m aicortexpy`(与 `aicortex` / `aicortexpy` 入口等价)。
13
+
14
+ | | |
15
+ |--|--|
16
+ | PyPI / 模块 | `aicortexpy` |
17
+ | Console scripts | `aicortex`、`aicortexpy`(`aicortex` 刻意同名,供 Agent 提示词零改动) |
18
+ | 配置 | 与 Go 共用 `~/.aicortex/config.json` |
19
+
20
+ ## 安装
21
+
22
+ ```powershell
23
+ pip install -U aicortexpy
24
+ # 源码:
25
+ cd python-daemon
26
+ pip install -e .
27
+ ```
28
+
29
+ ## 常用命令(对齐 Go CLI)
30
+
31
+ ```powershell
32
+ aicortex login --token mul_xxx
33
+ aicortex auth status
34
+ aicortex config show
35
+ aicortex workspace list --output json
36
+ aicortex issue get MUL-123 --output json
37
+ aicortex issue comment add MUL-123 --content "done"
38
+ aicortex daemon start
39
+ aicortex daemon status
40
+ aicortex version
41
+ ```
42
+
43
+ Agent 任务内:daemon 会把 `aicortex` shim 前置到 PATH,子进程执行 `aicortex issue ...` 即本包,无需 Go 二进制。
44
+
45
+ ## Daemon
46
+
47
+ ```powershell
48
+ $env:AICORTEX_DAEMON_BACKEND = "python" # 若仍用 Go 包装器拉起
49
+ aicortex daemon start
50
+ # 或
51
+ python -m aicortexpy daemon start --foreground
52
+ ```
53
+
54
+ Health 默认端口 **19514**。不要与 Go daemon 同时跑。
55
+
56
+ ## 发布
57
+
58
+ ```powershell
59
+ cd python-daemon
60
+ python -m build
61
+ python -m twine upload dist/*
62
+ ```
63
+
64
+ 版本:`pyproject.toml` 与 `aicortexpy/__init__.py` 的 `__version__` 保持一致。
65
+
66
+ ## 相对 Go CLI
67
+
68
+ 已覆盖 Agent/日常平台命令(issue/workspace/agent/squad/project/label/attachment/autopilot/meeting/skill/memory/graph/runtime/repo/auth/config/daemon)。
69
+
70
+ 尚未:浏览器 OAuth login、`update` 二进制自更新(请用 `pip install -U aicortexpy`)。
@@ -0,0 +1,3 @@
1
+ """AICortex Python CLI + local agent runtime daemon."""
2
+
3
+ __version__ = "0.2.1"
@@ -236,7 +236,9 @@ class ACPClient:
236
236
  if self.on_message:
237
237
  self.on_message(
238
238
  {
239
- "type": "tool-use",
239
+ # Server/UI wire format uses underscores (Go daemon maps
240
+ # internal "tool-use" → "tool_use" before ReportTaskMessages).
241
+ "type": "tool_use",
240
242
  "tool": tool_name,
241
243
  "call_id": call_id,
242
244
  "input": raw_input,
@@ -271,8 +273,18 @@ class ACPClient:
271
273
  output = str(data.get("rawOutput") or data.get("output") or "")
272
274
  if not output:
273
275
  output = _extract_tool_call_text(content)
276
+ tool_name = ""
277
+ if pending is not None:
278
+ tool_name = pending.tool_name or ""
274
279
  if self.on_message:
275
- self.on_message({"type": "tool-result", "call_id": call_id, "output": output})
280
+ self.on_message(
281
+ {
282
+ "type": "tool_result",
283
+ "tool": tool_name or "tool",
284
+ "call_id": call_id,
285
+ "output": output,
286
+ }
287
+ )
276
288
 
277
289
  def _emit_deferred_tool_use(self, pending: _PendingTool | None, call_id: str, data: dict) -> None:
278
290
  if pending is not None and pending.emitted:
@@ -300,7 +312,12 @@ class ACPClient:
300
312
  inp = {}
301
313
  if self.on_message:
302
314
  self.on_message(
303
- {"type": "tool-use", "tool": tool_name, "call_id": call_id, "input": inp}
315
+ {
316
+ "type": "tool_use",
317
+ "tool": tool_name,
318
+ "call_id": call_id,
319
+ "input": inp,
320
+ }
304
321
  )
305
322
 
306
323
  def _track_tool(self, call_id: str, p: _PendingTool) -> None:
@@ -226,9 +226,9 @@ class _BaseACPConn:
226
226
  typ = msg.get("type")
227
227
  if typ == "text":
228
228
  output_parts.append(str(msg.get("content") or ""))
229
- elif typ == "tool-use":
229
+ elif typ in ("tool_use", "tool-use"):
230
230
  in_flight_tools["n"] += 1
231
- elif typ == "tool-result":
231
+ elif typ in ("tool_result", "tool-result"):
232
232
  in_flight_tools["n"] = max(0, in_flight_tools["n"] - 1)
233
233
  messages.append(msg)
234
234
  if on_message:
@@ -33,12 +33,49 @@ def is_blocked_env_key(key: str) -> bool:
33
33
 
34
34
 
35
35
  def _aicortex_bin_dir() -> str | None:
36
- which = shutil.which("aicortexpy")
37
- if which:
38
- return str(Path(which).resolve().parent)
36
+ """Prefer Scripts dir that contains console_scripts aicortex / aicortexpy."""
37
+ for name in ("aicortex", "aicortexpy"):
38
+ which = shutil.which(name)
39
+ if which:
40
+ return str(Path(which).resolve().parent)
41
+ # Fallback: directory of this interpreter's Scripts (pip install --user / venv)
42
+ try:
43
+ import sysconfig
44
+
45
+ scripts = sysconfig.get_path("scripts")
46
+ if scripts and Path(scripts).is_dir():
47
+ return scripts
48
+ except Exception:
49
+ pass
39
50
  return None
40
51
 
41
52
 
53
+ def ensure_cli_shim_dir(state_dir: Path) -> Path | None:
54
+ """Write a tiny aicortex shim so agents resolve to python -m aicortexpy."""
55
+ shim_dir = state_dir / "py-cli-shim"
56
+ try:
57
+ shim_dir.mkdir(parents=True, exist_ok=True)
58
+ except OSError:
59
+ return None
60
+ import sys
61
+
62
+ py = sys.executable
63
+ if sys.platform == "win32":
64
+ cmd = shim_dir / "aicortex.cmd"
65
+ cmd.write_text(
66
+ f'@echo off\r\n"{py}" -m aicortexpy %*\r\n',
67
+ encoding="utf-8",
68
+ )
69
+ else:
70
+ sh = shim_dir / "aicortex"
71
+ sh.write_text(f'#!/bin/sh\nexec "{py}" -m aicortexpy "$@"\n', encoding="utf-8")
72
+ try:
73
+ sh.chmod(0o755)
74
+ except OSError:
75
+ pass
76
+ return shim_dir
77
+
78
+
42
79
  def build_agent_environ(
43
80
  base: dict[str, str],
44
81
  *,
@@ -55,7 +92,7 @@ def build_agent_environ(
55
92
 
56
93
  environ["AICORTEX_TOKEN"] = cfg.token
57
94
  environ["AICORTEX_SERVER_URL"] = cfg.server_base_url
58
- environ["aicortex_PORT"] = str(cfg.health_port)
95
+ environ["AICORTEX_DAEMON_PORT"] = str(cfg.health_port)
59
96
  environ["AICORTEX_WORKSPACE_ID"] = task.workspace_id or ""
60
97
  environ["AICORTEX_AGENT_NAME"] = agent_name
61
98
  environ["AICORTEX_AGENT_ID"] = agent_id
@@ -69,10 +106,16 @@ def build_agent_environ(
69
106
  if task.quick_create_prompt:
70
107
  environ["AICORTEX_QUICK_CREATE_TASK_ID"] = task.id
71
108
 
109
+ prepend: list[str] = []
110
+ shim = ensure_cli_shim_dir(cfg.state_dir)
111
+ if shim:
112
+ prepend.append(str(shim))
72
113
  bin_dir = _aicortex_bin_dir()
73
- if bin_dir:
114
+ if bin_dir and bin_dir not in prepend:
115
+ prepend.append(bin_dir)
116
+ if prepend:
74
117
  prev = environ.get("PATH") or os.environ.get("PATH") or ""
75
- environ["PATH"] = bin_dir + os.pathsep + prev
118
+ environ["PATH"] = os.pathsep.join(prepend) + os.pathsep + prev
76
119
 
77
120
  if extra:
78
121
  environ.update({str(k): str(v) for k, v in extra.items()})
@@ -61,7 +61,9 @@ def materialize_attachments(
61
61
  if not att_id:
62
62
  continue
63
63
  try:
64
- body, header_name = client.fetch_attachment_bytes(att_id)
64
+ body, header_name = client.fetch_attachment_bytes(
65
+ att_id, task.workspace_id
66
+ )
65
67
  except Exception as e:
66
68
  log.warning("fetch attachment failed id=%s: %s", att_id, e)
67
69
  continue
@@ -0,0 +1,177 @@
1
+ """Root CLI: python -m aicortexpy / aicortex — Go CLI compatible surface + daemon."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+
9
+ from . import __version__
10
+ from .commands import (
11
+ agent_cmd,
12
+ attachment_cmd,
13
+ auth_cmd,
14
+ autopilot_cmd,
15
+ config_cmd,
16
+ daemon_cmd,
17
+ graph_cmd,
18
+ issue_cmd,
19
+ label_cmd,
20
+ meeting_cmd,
21
+ memory_cmd,
22
+ project_cmd,
23
+ repo_cmd,
24
+ runtime_cmd,
25
+ setup_cmd,
26
+ skill_cmd,
27
+ squad_cmd,
28
+ update_cmd,
29
+ version_cmd,
30
+ workspace_cmd,
31
+ )
32
+
33
+ # Legacy top-level daemon verbs (pre-0.2) → forward to daemon *
34
+ _LEGACY_DAEMON = {
35
+ "start",
36
+ "run",
37
+ "stop",
38
+ "status",
39
+ "restart",
40
+ "capabilities",
41
+ "sandbox-info",
42
+ "disk-usage",
43
+ }
44
+
45
+
46
+ def _setup_logging(
47
+ verbose: bool = False,
48
+ *,
49
+ log_level: str = "",
50
+ daemon_mode: bool = False,
51
+ ) -> None:
52
+ """Configure root logging.
53
+
54
+ - CLI one-shots default to WARNING (quiet).
55
+ - Daemon run defaults to INFO (useful in python-daemon.log).
56
+ - `-v` or `--log-level DEBUG` enables DEBUG.
57
+ """
58
+ if log_level:
59
+ level = getattr(logging, log_level.upper(), logging.INFO)
60
+ elif verbose:
61
+ level = logging.DEBUG
62
+ elif daemon_mode:
63
+ level = logging.INFO
64
+ else:
65
+ level = logging.WARNING
66
+ logging.basicConfig(
67
+ level=level,
68
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
69
+ datefmt="%H:%M:%S",
70
+ force=True,
71
+ )
72
+
73
+
74
+ def build_parser() -> argparse.ArgumentParser:
75
+ p = argparse.ArgumentParser(
76
+ prog="aicortexpy",
77
+ description="AICortex Python CLI + local agent runtime (Go CLI compatible)",
78
+ )
79
+ p.add_argument("--version", action="version", version=f"aicortexpy {__version__}")
80
+ p.add_argument("--profile", default="", help="Configuration profile name")
81
+ p.add_argument("--server-url", default="", help="Override server URL")
82
+ p.add_argument("--workspace-id", default="", help="Override workspace id")
83
+ p.add_argument("-v", "--verbose", action="store_true")
84
+
85
+ sub = p.add_subparsers(dest="command", required=True)
86
+
87
+ issue_cmd.add_parser(sub)
88
+ agent_cmd.add_parser(sub)
89
+ workspace_cmd.add_parser(sub)
90
+ project_cmd.add_parser(sub)
91
+ squad_cmd.add_parser(sub)
92
+ label_cmd.add_parser(sub)
93
+ autopilot_cmd.add_parser(sub)
94
+ meeting_cmd.add_parser(sub)
95
+ skill_cmd.add_parser(sub)
96
+ memory_cmd.add_parser(sub)
97
+ graph_cmd.add_parser(sub)
98
+ repo_cmd.add_parser(sub)
99
+ attachment_cmd.add_parser(sub)
100
+ auth_cmd.add_parser(sub)
101
+ config_cmd.add_parser(sub)
102
+ daemon_cmd.add_parser(sub)
103
+ runtime_cmd.add_parser(sub)
104
+ setup_cmd.add_parser(sub)
105
+ update_cmd.add_parser(sub)
106
+ version_cmd.add_parser(sub)
107
+ return p
108
+
109
+
110
+ def main(argv: list[str] | None = None) -> int:
111
+ argv = list(sys.argv[1:] if argv is None else argv)
112
+ # rewrite legacy: aicortexpy start → daemon start
113
+ if argv and argv[0] in _LEGACY_DAEMON:
114
+ print(
115
+ f"note: `{argv[0]}` is deprecated; use `aicortexpy daemon {argv[0]}`",
116
+ file=sys.stderr,
117
+ )
118
+ argv = ["daemon", *argv]
119
+
120
+ parser = build_parser()
121
+ try:
122
+ ns = parser.parse_args(argv)
123
+ except SystemExit as e:
124
+ return int(e.code) if isinstance(e.code, int) else 1
125
+
126
+ cmd = ns.command
127
+ _setup_logging(
128
+ getattr(ns, "verbose", False),
129
+ log_level=str(getattr(ns, "log_level", "") or ""),
130
+ daemon_mode=(
131
+ cmd == "daemon"
132
+ and getattr(ns, "daemon_command", "") in {"start", "run", "restart"}
133
+ ),
134
+ )
135
+
136
+ handlers = {
137
+ "issue": issue_cmd.run,
138
+ "agent": agent_cmd.run,
139
+ "workspace": workspace_cmd.run,
140
+ "project": project_cmd.run,
141
+ "squad": squad_cmd.run,
142
+ "label": label_cmd.run,
143
+ "autopilot": autopilot_cmd.run,
144
+ "meeting": meeting_cmd.run,
145
+ "skill": skill_cmd.run,
146
+ "memory": memory_cmd.run,
147
+ "graph": graph_cmd.run,
148
+ "repo": repo_cmd.run,
149
+ "attachment": attachment_cmd.run,
150
+ "config": config_cmd.run,
151
+ "daemon": daemon_cmd.run,
152
+ "runtime": runtime_cmd.run,
153
+ "version": version_cmd.run,
154
+ "login": auth_cmd.run_login,
155
+ "auth": auth_cmd.run_auth,
156
+ "setup": setup_cmd.run,
157
+ "update": update_cmd.run,
158
+ }
159
+ handler = handlers.get(cmd)
160
+ if not handler:
161
+ parser.print_help()
162
+ return 2
163
+ try:
164
+ return int(handler(ns) or 0)
165
+ except SystemExit as e:
166
+ if e.code not in (0, None):
167
+ msg = e.args[0] if e.args else str(e.code)
168
+ if isinstance(msg, str) and msg not in {"0", "1", "2"}:
169
+ print(msg, file=sys.stderr)
170
+ return int(e.code) if isinstance(e.code, int) else 1
171
+ return 0
172
+ except KeyboardInterrupt:
173
+ return 130
174
+
175
+
176
+ if __name__ == "__main__":
177
+ raise SystemExit(main())
@@ -0,0 +1,99 @@
1
+ """Resolve PlatformClient from flags / env / ~/.aicortex/config.json."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from . import __version__
12
+ from .config import (
13
+ CLIConfig,
14
+ DEFAULT_SERVER_URL,
15
+ cli_config_path,
16
+ load_cli_config,
17
+ normalize_http_base,
18
+ profile_dir,
19
+ )
20
+ from .platform_client import PlatformClient
21
+
22
+
23
+ def save_cli_config(cfg: CLIConfig, profile: str = "") -> Path:
24
+ path = cli_config_path(profile)
25
+ path.parent.mkdir(parents=True, exist_ok=True)
26
+ data: dict[str, Any] = {}
27
+ if cfg.server_url:
28
+ data["server_url"] = cfg.server_url
29
+ if cfg.app_url:
30
+ data["app_url"] = cfg.app_url
31
+ if cfg.workspace_id:
32
+ data["workspace_id"] = cfg.workspace_id
33
+ if cfg.token:
34
+ data["token"] = cfg.token
35
+ tmp = path.with_suffix(".json.tmp")
36
+ tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
37
+ tmp.replace(path)
38
+ return path
39
+
40
+
41
+ def profile_of(ns: argparse.Namespace) -> str:
42
+ return str(getattr(ns, "profile", None) or "").strip()
43
+
44
+
45
+ def resolve_server_url(ns: argparse.Namespace) -> str:
46
+ flag = str(getattr(ns, "server_url", None) or "").strip()
47
+ if flag:
48
+ return normalize_http_base(flag)
49
+ env = os.environ.get("AICORTEX_SERVER_URL", "").strip()
50
+ if env:
51
+ return normalize_http_base(env)
52
+ cfg = load_cli_config(profile_of(ns))
53
+ return normalize_http_base(cfg.server_url or DEFAULT_SERVER_URL)
54
+
55
+
56
+ def resolve_workspace_id(ns: argparse.Namespace) -> str:
57
+ flag = str(getattr(ns, "workspace_id", None) or "").strip()
58
+ if flag:
59
+ return flag
60
+ env = os.environ.get("AICORTEX_WORKSPACE_ID", "").strip()
61
+ if env:
62
+ return env
63
+ return load_cli_config(profile_of(ns)).workspace_id
64
+
65
+
66
+ def resolve_token(ns: argparse.Namespace) -> str:
67
+ env = os.environ.get("AICORTEX_TOKEN", "").strip()
68
+ if env:
69
+ return env
70
+ return load_cli_config(profile_of(ns)).token
71
+
72
+
73
+ def require_client(ns: argparse.Namespace, *, require_workspace: bool = True) -> PlatformClient:
74
+ token = resolve_token(ns)
75
+ if not token:
76
+ raise SystemExit("Not authenticated. Run `aicortexpy login --token mul_...` or set AICORTEX_TOKEN.")
77
+ ws = resolve_workspace_id(ns)
78
+ if require_workspace and not ws:
79
+ raise SystemExit("No workspace_id. Set via `aicortexpy config set workspace_id <id>` or --workspace-id.")
80
+ return PlatformClient(
81
+ resolve_server_url(ns),
82
+ token,
83
+ ws,
84
+ version=__version__,
85
+ )
86
+
87
+
88
+ def read_text_flag(ns: argparse.Namespace, name: str = "content") -> str:
89
+ """Resolve --content / --content-stdin / --content-file (and description*)."""
90
+ file_attr = f"{name}_file"
91
+ stdin_attr = f"{name}_stdin"
92
+ if getattr(ns, stdin_attr, False):
93
+ import sys
94
+
95
+ return sys.stdin.read()
96
+ path = getattr(ns, file_attr, None)
97
+ if path:
98
+ return Path(path).read_text(encoding="utf-8")
99
+ return str(getattr(ns, name, None) or "")
@@ -422,10 +422,17 @@ class DaemonClient:
422
422
  data = self.get(f"/api/daemon/workspaces/{workspace_id}/repos")
423
423
  return data if isinstance(data, dict) else {}
424
424
 
425
- def fetch_attachment_bytes(self, attachment_id: str) -> tuple[bytes, str]:
425
+ def fetch_attachment_bytes(
426
+ self, attachment_id: str, workspace_id: str = ""
427
+ ) -> tuple[bytes, str]:
426
428
  path = f"/api/daemon/attachments/{attachment_id}/bytes"
427
429
  url = self.base_url + path
428
- req = urllib.request.Request(url, headers=self._headers(), method="GET")
430
+ headers = self._headers()
431
+ wid = (workspace_id or "").strip()
432
+ if wid:
433
+ # PAT/JWT daemon auth has no workspace in context; server requires it.
434
+ headers["X-Workspace-ID"] = wid
435
+ req = urllib.request.Request(url, headers=headers, method="GET")
429
436
  try:
430
437
  with urllib.request.urlopen(req, timeout=self.timeout, context=self._ctx) as resp:
431
438
  body = resp.read(MAX_ATTACHMENT_BYTES + 1)
@@ -0,0 +1 @@
1
+ """CLI command modules."""