aicortexpy 0.2.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.
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/PKG-INFO +1 -1
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/__init__.py +1 -1
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/acp_client.py +20 -3
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/acp_conn.py +2 -2
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/attachments.py +3 -1
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/cli.py +35 -3
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/client.py +9 -2
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/agent_cmd.py +37 -0
- aicortexpy-0.2.1/aicortexpy/commands/auth_cmd.py +167 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/autopilot_cmd.py +86 -2
- aicortexpy-0.2.1/aicortexpy/commands/daemon_cmd.py +228 -0
- aicortexpy-0.2.1/aicortexpy/commands/setup_cmd.py +159 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/skill_cmd.py +28 -6
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/squad_cmd.py +22 -1
- aicortexpy-0.2.1/aicortexpy/commands/update_cmd.py +46 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/config.py +81 -7
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/id_resolve.py +17 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/models.py +8 -0
- aicortexpy-0.2.1/aicortexpy/oauth_login.py +183 -0
- aicortexpy-0.2.1/aicortexpy/output.py +77 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/platform_client.py +8 -1
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/process.py +2 -1
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/runner.py +50 -2
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/PKG-INFO +1 -1
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/SOURCES.txt +3 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/pyproject.toml +1 -1
- aicortexpy-0.2.0/aicortexpy/commands/auth_cmd.py +0 -87
- aicortexpy-0.2.0/aicortexpy/commands/daemon_cmd.py +0 -97
- aicortexpy-0.2.0/aicortexpy/output.py +0 -41
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/MANIFEST.in +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/README.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/__main__.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/acp_errors.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/agent_env.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/artifact_ws.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/artifacts.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/auto_commit.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/capabilities.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/chat_acp_pool.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/cli_context.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/codex_config.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/__init__.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/attachment_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/config_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/graph_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/issue_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/label_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/meeting_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/memory_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/project_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/repo_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/runtime_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/version_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/commands/workspace_cmd.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/daemon.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/design_seed.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/diskusage.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/execenv.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/gc.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/handoff_bundle.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/health.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/local_skills.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/openclaw_config.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/pinned_workdir.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/platform_info.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/poisoned.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/procutil.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/prompt.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/repo.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/runtime_config.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/sandbox.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/stream.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/terminal.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/tunnel.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy/wakeup.py +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/dependency_links.txt +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/entry_points.txt +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/requires.txt +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/aicortexpy.egg-info/top_level.txt +0 -0
- {aicortexpy-0.2.0 → aicortexpy-0.2.1}/setup.cfg +0 -0
|
@@ -236,7 +236,9 @@ class ACPClient:
|
|
|
236
236
|
if self.on_message:
|
|
237
237
|
self.on_message(
|
|
238
238
|
{
|
|
239
|
-
|
|
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(
|
|
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
|
-
{
|
|
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
|
|
229
|
+
elif typ in ("tool_use", "tool-use"):
|
|
230
230
|
in_flight_tools["n"] += 1
|
|
231
|
-
elif typ
|
|
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:
|
|
@@ -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(
|
|
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
|
|
@@ -22,8 +22,10 @@ from .commands import (
|
|
|
22
22
|
project_cmd,
|
|
23
23
|
repo_cmd,
|
|
24
24
|
runtime_cmd,
|
|
25
|
+
setup_cmd,
|
|
25
26
|
skill_cmd,
|
|
26
27
|
squad_cmd,
|
|
28
|
+
update_cmd,
|
|
27
29
|
version_cmd,
|
|
28
30
|
workspace_cmd,
|
|
29
31
|
)
|
|
@@ -41,12 +43,31 @@ _LEGACY_DAEMON = {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
|
|
44
|
-
def _setup_logging(
|
|
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
|
|
46
66
|
logging.basicConfig(
|
|
47
67
|
level=level,
|
|
48
68
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
49
69
|
datefmt="%H:%M:%S",
|
|
70
|
+
force=True,
|
|
50
71
|
)
|
|
51
72
|
|
|
52
73
|
|
|
@@ -80,6 +101,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
80
101
|
config_cmd.add_parser(sub)
|
|
81
102
|
daemon_cmd.add_parser(sub)
|
|
82
103
|
runtime_cmd.add_parser(sub)
|
|
104
|
+
setup_cmd.add_parser(sub)
|
|
105
|
+
update_cmd.add_parser(sub)
|
|
83
106
|
version_cmd.add_parser(sub)
|
|
84
107
|
return p
|
|
85
108
|
|
|
@@ -100,8 +123,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
100
123
|
except SystemExit as e:
|
|
101
124
|
return int(e.code) if isinstance(e.code, int) else 1
|
|
102
125
|
|
|
103
|
-
_setup_logging(getattr(ns, "verbose", False))
|
|
104
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
|
+
)
|
|
105
135
|
|
|
106
136
|
handlers = {
|
|
107
137
|
"issue": issue_cmd.run,
|
|
@@ -123,6 +153,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
123
153
|
"version": version_cmd.run,
|
|
124
154
|
"login": auth_cmd.run_login,
|
|
125
155
|
"auth": auth_cmd.run_auth,
|
|
156
|
+
"setup": setup_cmd.run,
|
|
157
|
+
"update": update_cmd.run,
|
|
126
158
|
}
|
|
127
159
|
handler = handlers.get(cmd)
|
|
128
160
|
if not handler:
|
|
@@ -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(
|
|
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
|
-
|
|
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)
|
|
@@ -4,12 +4,16 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import sys
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from ..cli_context import require_client
|
|
9
10
|
from ..id_resolve import list_agents, resolve_by_prefix
|
|
10
11
|
from ..output import print_json, str_val
|
|
11
12
|
from ..platform_client import PlatformError, q
|
|
12
13
|
|
|
14
|
+
_AVATAR_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
|
|
15
|
+
_AVATAR_MAX = 5 << 20 # 5 MiB
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
15
19
|
p = sub.add_parser("agent", help="Work with agents")
|
|
@@ -37,6 +41,10 @@ def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
|
37
41
|
tasks = asub.add_parser("tasks", help="List agent tasks")
|
|
38
42
|
tasks.add_argument("agent_id")
|
|
39
43
|
tasks.add_argument("--output", choices=["json", "table"], default="json")
|
|
44
|
+
avatar = asub.add_parser("avatar", help="Upload an avatar image for an agent")
|
|
45
|
+
avatar.add_argument("agent_id")
|
|
46
|
+
avatar.add_argument("--file", required=True, help="Path to avatar image (.png/.jpg/.gif/.webp)")
|
|
47
|
+
avatar.add_argument("--output", choices=["json", "table"], default="json")
|
|
40
48
|
skills = asub.add_parser("skills", help="Agent skills")
|
|
41
49
|
ssub = skills.add_subparsers(dest="agent_skills_command", required=True)
|
|
42
50
|
sl = ssub.add_parser("list")
|
|
@@ -95,6 +103,35 @@ def run(ns: argparse.Namespace) -> int:
|
|
|
95
103
|
if ns.agent_command == "tasks":
|
|
96
104
|
print_json(client.get(f"/api/agents/{_aid(client, ns.agent_id)}/tasks"))
|
|
97
105
|
return 0
|
|
106
|
+
if ns.agent_command == "avatar":
|
|
107
|
+
path = Path(ns.file)
|
|
108
|
+
if not path.is_file():
|
|
109
|
+
print(f"file not found: {path}", file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
ext = path.suffix.lower()
|
|
112
|
+
if ext not in _AVATAR_EXTS:
|
|
113
|
+
print(
|
|
114
|
+
f"unsupported file format {ext!r}: must be .png, .jpg, .jpeg, .gif, or .webp",
|
|
115
|
+
file=sys.stderr,
|
|
116
|
+
)
|
|
117
|
+
return 1
|
|
118
|
+
data = path.read_bytes()
|
|
119
|
+
if len(data) > _AVATAR_MAX:
|
|
120
|
+
print(f"file too large: {len(data)} bytes (max 5MB)", file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
aid = _aid(client, ns.agent_id)
|
|
123
|
+
client.get(f"/api/agents/{aid}") # existence check
|
|
124
|
+
att_id, url = client.upload_file_with_url(data, str(path))
|
|
125
|
+
if not url:
|
|
126
|
+
print("upload succeeded but no url returned", file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
client.put(f"/api/agents/{aid}", {"avatar_url": url})
|
|
129
|
+
result = {"id": att_id, "agent_id": aid, "avatar_url": url}
|
|
130
|
+
if ns.output == "json":
|
|
131
|
+
print_json(result)
|
|
132
|
+
else:
|
|
133
|
+
print(f"{att_id}\t{aid}\t{url}")
|
|
134
|
+
return 0
|
|
98
135
|
if ns.agent_command == "skills":
|
|
99
136
|
aid = _aid(client, ns.agent_id)
|
|
100
137
|
if ns.agent_skills_command == "list":
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""login / auth commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import getpass
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .. import __version__
|
|
11
|
+
from ..cli_context import profile_of, resolve_server_url, resolve_token, save_cli_config
|
|
12
|
+
from ..config import load_cli_config
|
|
13
|
+
from ..oauth_login import browser_login
|
|
14
|
+
from ..platform_client import PlatformClient, PlatformError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
18
|
+
login = sub.add_parser(
|
|
19
|
+
"login",
|
|
20
|
+
help="Authenticate (browser OAuth, or --token for PAT)",
|
|
21
|
+
)
|
|
22
|
+
login.add_argument(
|
|
23
|
+
"--token",
|
|
24
|
+
nargs="?",
|
|
25
|
+
const="",
|
|
26
|
+
default=None,
|
|
27
|
+
help="PAT (mul_...); pass --token alone to prompt; omit for browser OAuth",
|
|
28
|
+
)
|
|
29
|
+
login.add_argument(
|
|
30
|
+
"--callback-host",
|
|
31
|
+
default="",
|
|
32
|
+
help="Host the OAuth callback URL points at (auto-detected when empty)",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
auth = sub.add_parser("auth", help="Authenticate aicortexpy with AICortex")
|
|
36
|
+
asub = auth.add_subparsers(dest="auth_command", required=True)
|
|
37
|
+
asub.add_parser("status", help="Show authentication status")
|
|
38
|
+
asub.add_parser("logout", help="Remove stored token")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_app_url(ns: argparse.Namespace) -> str:
|
|
42
|
+
env = (
|
|
43
|
+
os.environ.get("AICORTEX_APP_URL", "").strip()
|
|
44
|
+
or os.environ.get("FRONTEND_ORIGIN", "").strip()
|
|
45
|
+
)
|
|
46
|
+
if env:
|
|
47
|
+
return env.rstrip("/")
|
|
48
|
+
cfg = load_cli_config(profile_of(ns))
|
|
49
|
+
return (cfg.app_url or "").rstrip("/")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _save_token_and_workspaces(ns: argparse.Namespace, token: str, server: str) -> int:
|
|
53
|
+
client = PlatformClient(server, token, "", version=__version__)
|
|
54
|
+
try:
|
|
55
|
+
me = client.get("/api/me")
|
|
56
|
+
except PlatformError as e:
|
|
57
|
+
print(f"invalid token: {e}", file=sys.stderr)
|
|
58
|
+
return 1
|
|
59
|
+
profile = profile_of(ns)
|
|
60
|
+
cfg = load_cli_config(profile)
|
|
61
|
+
cfg.token = token
|
|
62
|
+
cfg.server_url = server
|
|
63
|
+
if not cfg.app_url:
|
|
64
|
+
cfg.app_url = _resolve_app_url(ns)
|
|
65
|
+
cfg.workspace_id = ""
|
|
66
|
+
try:
|
|
67
|
+
workspaces = client.get("/api/workspaces")
|
|
68
|
+
if isinstance(workspaces, list) and workspaces:
|
|
69
|
+
cfg.workspace_id = str(workspaces[0].get("id") or "")
|
|
70
|
+
print(f"\nFound {len(workspaces)} workspace(s):", file=sys.stderr)
|
|
71
|
+
for ws in workspaces:
|
|
72
|
+
print(f" - {ws.get('name')} ({ws.get('id')})", file=sys.stderr)
|
|
73
|
+
else:
|
|
74
|
+
print("\nNo workspaces found.", file=sys.stderr)
|
|
75
|
+
app = cfg.app_url or _resolve_app_url(ns)
|
|
76
|
+
if app:
|
|
77
|
+
print(
|
|
78
|
+
f"Create a workspace in the web dashboard: {app}/workspaces/new",
|
|
79
|
+
file=sys.stderr,
|
|
80
|
+
)
|
|
81
|
+
except PlatformError:
|
|
82
|
+
pass
|
|
83
|
+
save_cli_config(cfg, profile)
|
|
84
|
+
name = (me or {}).get("name") if isinstance(me, dict) else ""
|
|
85
|
+
email = (me or {}).get("email") if isinstance(me, dict) else ""
|
|
86
|
+
print(f"Authenticated as {name} ({email})\nToken saved to config.", file=sys.stderr)
|
|
87
|
+
print("\n→ Run 'aicortex daemon start' to start your local agent runtime.", file=sys.stderr)
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run_login(ns: argparse.Namespace) -> int:
|
|
92
|
+
# --token present (even empty const) → PAT path; otherwise browser OAuth.
|
|
93
|
+
if ns.token is not None:
|
|
94
|
+
token = ns.token
|
|
95
|
+
if token == "":
|
|
96
|
+
token = getpass.getpass("Enter your personal access token: ").strip()
|
|
97
|
+
token = (token or "").strip()
|
|
98
|
+
if not token.startswith("mul_"):
|
|
99
|
+
print("invalid token format: must start with mul_", file=sys.stderr)
|
|
100
|
+
return 1
|
|
101
|
+
return _save_token_and_workspaces(ns, token, resolve_server_url(ns))
|
|
102
|
+
|
|
103
|
+
server = resolve_server_url(ns)
|
|
104
|
+
app = _resolve_app_url(ns)
|
|
105
|
+
profile = profile_of(ns)
|
|
106
|
+
try:
|
|
107
|
+
browser_login(
|
|
108
|
+
server_url=server,
|
|
109
|
+
app_url=app,
|
|
110
|
+
profile=profile,
|
|
111
|
+
callback_host_flag=getattr(ns, "callback_host", "") or "",
|
|
112
|
+
)
|
|
113
|
+
except SystemExit as e:
|
|
114
|
+
if e.args and isinstance(e.args[0], str):
|
|
115
|
+
print(e.args[0], file=sys.stderr)
|
|
116
|
+
return 1 if e.code not in (0, None) else 0
|
|
117
|
+
|
|
118
|
+
# Pick default workspace after OAuth saved the token.
|
|
119
|
+
cfg = load_cli_config(profile)
|
|
120
|
+
token = cfg.token
|
|
121
|
+
if not token:
|
|
122
|
+
return 1
|
|
123
|
+
client = PlatformClient(server, token, "", version=__version__)
|
|
124
|
+
try:
|
|
125
|
+
workspaces = client.get("/api/workspaces")
|
|
126
|
+
if isinstance(workspaces, list) and workspaces:
|
|
127
|
+
cfg.workspace_id = str(workspaces[0].get("id") or "")
|
|
128
|
+
save_cli_config(cfg, profile)
|
|
129
|
+
print(f"\nFound {len(workspaces)} workspace(s):", file=sys.stderr)
|
|
130
|
+
for ws in workspaces:
|
|
131
|
+
print(f" - {ws.get('name')} ({ws.get('id')})", file=sys.stderr)
|
|
132
|
+
except PlatformError:
|
|
133
|
+
pass
|
|
134
|
+
print("\n→ Run 'aicortex daemon start' to start your local agent runtime.", file=sys.stderr)
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_auth(ns: argparse.Namespace) -> int:
|
|
139
|
+
profile = profile_of(ns)
|
|
140
|
+
if ns.auth_command == "logout":
|
|
141
|
+
cfg = load_cli_config(profile)
|
|
142
|
+
cfg.token = ""
|
|
143
|
+
save_cli_config(cfg, profile)
|
|
144
|
+
print("Logged out.", file=sys.stderr)
|
|
145
|
+
return 0
|
|
146
|
+
token = resolve_token(ns)
|
|
147
|
+
server = resolve_server_url(ns)
|
|
148
|
+
if not token:
|
|
149
|
+
print(
|
|
150
|
+
"Not authenticated. Run 'aicortexpy login' (browser) or 'aicortexpy login --token mul_...'.",
|
|
151
|
+
file=sys.stderr,
|
|
152
|
+
)
|
|
153
|
+
return 0
|
|
154
|
+
client = PlatformClient(server, token, "", version=__version__)
|
|
155
|
+
try:
|
|
156
|
+
me = client.get("/api/me")
|
|
157
|
+
except PlatformError as e:
|
|
158
|
+
print(
|
|
159
|
+
f"Token is invalid or expired: {e}\nRun 'aicortexpy login' to re-authenticate.",
|
|
160
|
+
file=sys.stderr,
|
|
161
|
+
)
|
|
162
|
+
return 0
|
|
163
|
+
prefix = token[:12] + "..." if len(token) > 12 else token
|
|
164
|
+
name = (me or {}).get("name") if isinstance(me, dict) else ""
|
|
165
|
+
email = (me or {}).get("email") if isinstance(me, dict) else ""
|
|
166
|
+
print(f"Server: {server}\nUser: {name} ({email})\nToken: {prefix}", file=sys.stderr)
|
|
167
|
+
return 0
|
|
@@ -6,7 +6,12 @@ import argparse
|
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
8
|
from ..cli_context import require_client
|
|
9
|
-
from ..id_resolve import
|
|
9
|
+
from ..id_resolve import (
|
|
10
|
+
list_agents,
|
|
11
|
+
resolve_autopilot,
|
|
12
|
+
resolve_autopilot_trigger,
|
|
13
|
+
resolve_by_prefix,
|
|
14
|
+
)
|
|
10
15
|
from ..output import print_json
|
|
11
16
|
from ..platform_client import PlatformError, q
|
|
12
17
|
|
|
@@ -36,7 +41,7 @@ def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
|
36
41
|
update.add_argument("--output", choices=["json", "table"], default="json")
|
|
37
42
|
delete = asub.add_parser("delete")
|
|
38
43
|
delete.add_argument("autopilot_id")
|
|
39
|
-
trigger = asub.add_parser("trigger")
|
|
44
|
+
trigger = asub.add_parser("trigger", help="Manually trigger an autopilot once")
|
|
40
45
|
trigger.add_argument("autopilot_id")
|
|
41
46
|
trigger.add_argument("--output", choices=["json", "table"], default="json")
|
|
42
47
|
runs = asub.add_parser("runs")
|
|
@@ -44,6 +49,37 @@ def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
|
44
49
|
runs.add_argument("--limit", type=int, default=50)
|
|
45
50
|
runs.add_argument("--output", choices=["json", "table"], default="json")
|
|
46
51
|
|
|
52
|
+
tadd = asub.add_parser("trigger-add", help="Add a schedule trigger to an autopilot")
|
|
53
|
+
tadd.add_argument("autopilot_id")
|
|
54
|
+
tadd.add_argument("--cron", required=True, help="Cron expression (required)")
|
|
55
|
+
tadd.add_argument("--timezone", default="", help="IANA timezone (default UTC)")
|
|
56
|
+
tadd.add_argument("--label", default="", help="Optional human-readable label")
|
|
57
|
+
tadd.add_argument("--output", choices=["json", "table"], default="json")
|
|
58
|
+
|
|
59
|
+
tupd = asub.add_parser("trigger-update", help="Update an existing trigger")
|
|
60
|
+
tupd.add_argument("autopilot_id")
|
|
61
|
+
tupd.add_argument("trigger_id")
|
|
62
|
+
tupd.add_argument("--enabled", default=None, choices=["true", "false", "1", "0", "yes", "no"])
|
|
63
|
+
tupd.add_argument("--cron", default=None)
|
|
64
|
+
tupd.add_argument("--timezone", default=None)
|
|
65
|
+
tupd.add_argument("--label", default=None)
|
|
66
|
+
tupd.add_argument("--output", choices=["json", "table"], default="json")
|
|
67
|
+
|
|
68
|
+
tdel = asub.add_parser("trigger-delete", help="Delete a trigger")
|
|
69
|
+
tdel.add_argument("autopilot_id")
|
|
70
|
+
tdel.add_argument("trigger_id")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_bool(raw: str | None) -> bool | None:
|
|
74
|
+
if raw is None:
|
|
75
|
+
return None
|
|
76
|
+
v = raw.strip().lower()
|
|
77
|
+
if v in {"true", "1", "yes", "y", "on"}:
|
|
78
|
+
return True
|
|
79
|
+
if v in {"false", "0", "no", "n", "off"}:
|
|
80
|
+
return False
|
|
81
|
+
raise SystemExit(f"invalid boolean value: {raw!r}")
|
|
82
|
+
|
|
47
83
|
|
|
48
84
|
def run(ns: argparse.Namespace) -> int:
|
|
49
85
|
client = require_client(ns)
|
|
@@ -94,6 +130,54 @@ def run(ns: argparse.Namespace) -> int:
|
|
|
94
130
|
aid = resolve_autopilot(client, ns.autopilot_id)
|
|
95
131
|
print_json(client.get(f"/api/autopilots/{aid}/runs{q({'limit': ns.limit})}"))
|
|
96
132
|
return 0
|
|
133
|
+
if ns.autopilot_command == "trigger-add":
|
|
134
|
+
aid = resolve_autopilot(client, ns.autopilot_id)
|
|
135
|
+
body: dict = {
|
|
136
|
+
"kind": "schedule",
|
|
137
|
+
"cron_expression": ns.cron,
|
|
138
|
+
}
|
|
139
|
+
if ns.timezone:
|
|
140
|
+
body["timezone"] = ns.timezone
|
|
141
|
+
if ns.label:
|
|
142
|
+
body["label"] = ns.label
|
|
143
|
+
result = client.post(f"/api/autopilots/{aid}/triggers", body)
|
|
144
|
+
if ns.output == "json":
|
|
145
|
+
print_json(result)
|
|
146
|
+
else:
|
|
147
|
+
rid = (result or {}).get("id") if isinstance(result, dict) else ""
|
|
148
|
+
print(f"Trigger created: {rid}")
|
|
149
|
+
return 0
|
|
150
|
+
if ns.autopilot_command == "trigger-update":
|
|
151
|
+
aid = resolve_autopilot(client, ns.autopilot_id)
|
|
152
|
+
tid = resolve_autopilot_trigger(client, aid, ns.trigger_id)
|
|
153
|
+
body = {}
|
|
154
|
+
enabled = _parse_bool(ns.enabled)
|
|
155
|
+
if enabled is not None:
|
|
156
|
+
body["enabled"] = enabled
|
|
157
|
+
if ns.cron is not None:
|
|
158
|
+
body["cron_expression"] = ns.cron
|
|
159
|
+
if ns.timezone is not None:
|
|
160
|
+
body["timezone"] = ns.timezone
|
|
161
|
+
if ns.label is not None:
|
|
162
|
+
body["label"] = ns.label
|
|
163
|
+
if not body:
|
|
164
|
+
print(
|
|
165
|
+
"no fields to update; use --enabled, --cron, --timezone, or --label",
|
|
166
|
+
file=sys.stderr,
|
|
167
|
+
)
|
|
168
|
+
return 1
|
|
169
|
+
result = client.patch(f"/api/autopilots/{aid}/triggers/{tid}", body)
|
|
170
|
+
if ns.output == "json":
|
|
171
|
+
print_json(result)
|
|
172
|
+
else:
|
|
173
|
+
print(f"Trigger updated: {tid}")
|
|
174
|
+
return 0
|
|
175
|
+
if ns.autopilot_command == "trigger-delete":
|
|
176
|
+
aid = resolve_autopilot(client, ns.autopilot_id)
|
|
177
|
+
tid = resolve_autopilot_trigger(client, aid, ns.trigger_id)
|
|
178
|
+
client.delete(f"/api/autopilots/{aid}/triggers/{tid}")
|
|
179
|
+
print(f"Trigger {tid} deleted.", file=sys.stderr)
|
|
180
|
+
return 0
|
|
97
181
|
except PlatformError as e:
|
|
98
182
|
print(str(e), file=sys.stderr)
|
|
99
183
|
return 1
|