aigenora 0.0.1__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.
- aigenora/__init__.py +2 -0
- aigenora/__main__.py +6 -0
- aigenora/agent/__init__.py +1 -0
- aigenora/agent/_daemon.py +96 -0
- aigenora/agent/_web_mode.py +52 -0
- aigenora/agent/bootstrap.py +151 -0
- aigenora/agent/browse.py +120 -0
- aigenora/agent/cancel.py +14 -0
- aigenora/agent/console.py +265 -0
- aigenora/agent/doctor.py +26 -0
- aigenora/agent/elo.py +63 -0
- aigenora/agent/feedback.py +34 -0
- aigenora/agent/guest.py +29 -0
- aigenora/agent/host.py +347 -0
- aigenora/agent/inbox.py +162 -0
- aigenora/agent/init.py +108 -0
- aigenora/agent/join.py +272 -0
- aigenora/agent/karma.py +70 -0
- aigenora/agent/protocol.py +509 -0
- aigenora/agent/protocol_adversarial.py +214 -0
- aigenora/agent/protocol_preflight.py +182 -0
- aigenora/agent/protocol_search.py +217 -0
- aigenora/agent/protocol_ui.py +243 -0
- aigenora/agent/register.py +30 -0
- aigenora/agent/registry.py +76 -0
- aigenora/agent/session.py +594 -0
- aigenora/agent/skeleton.py +538 -0
- aigenora/agent/skill.py +391 -0
- aigenora/agent/trust.py +221 -0
- aigenora/agent/validate.py +17 -0
- aigenora/agent/web.py +1621 -0
- aigenora/cli.py +558 -0
- aigenora/engine/__init__.py +1 -0
- aigenora/engine/box.py +95 -0
- aigenora/engine/config.py +98 -0
- aigenora/engine/crypto.py +139 -0
- aigenora/engine/keys.py +100 -0
- aigenora/engine/p2p.py +418 -0
- aigenora/engine/rest.py +34 -0
- aigenora/proto/__init__.py +1 -0
- aigenora/proto/decide_gateway.py +122 -0
- aigenora/proto/engine.py +1761 -0
- aigenora/proto/hooks.py +133 -0
- aigenora/proto/loader.py +28 -0
- aigenora/proto/prefs.py +142 -0
- aigenora/proto/sdk.py +685 -0
- aigenora/proto/session.py +115 -0
- aigenora/proto/spec_version.py +29 -0
- aigenora/proto/validate.py +271 -0
- aigenora/protocols/166570ef/f5c0864d31ccafb9d04ea5154184542085dfa401a9c3590f6831e8c8/hooks.py +227 -0
- aigenora/protocols/166570ef/f5c0864d31ccafb9d04ea5154184542085dfa401a9c3590f6831e8c8/spec.json +237 -0
- aigenora/protocols/166570ef/f5c0864d31ccafb9d04ea5154184542085dfa401a9c3590f6831e8c8/ui/index.html +317 -0
- aigenora/protocols/5358130e/14885f61ff210d2dd44c58a15ca4140bd89558f41f3eb867bc188e8f/hooks.py +342 -0
- aigenora/protocols/5358130e/14885f61ff210d2dd44c58a15ca4140bd89558f41f3eb867bc188e8f/spec.json +126 -0
- aigenora/protocols/5358130e/14885f61ff210d2dd44c58a15ca4140bd89558f41f3eb867bc188e8f/ui/index.html +192 -0
- aigenora/protocols/59da01bc/80d2ac385e0c9c642ad578ffa83b8fe273c4df21d80b2555437b8a31/hooks.py +83 -0
- aigenora/protocols/59da01bc/80d2ac385e0c9c642ad578ffa83b8fe273c4df21d80b2555437b8a31/spec.json +52 -0
- aigenora/protocols/59da01bc/80d2ac385e0c9c642ad578ffa83b8fe273c4df21d80b2555437b8a31/ui/index.html +165 -0
- aigenora/protocols/5cd50a30/977f690c81073e861c9fec9323b3bf359e703c886ff9b0153c1cb209/hooks.py +292 -0
- aigenora/protocols/5cd50a30/977f690c81073e861c9fec9323b3bf359e703c886ff9b0153c1cb209/spec.json +114 -0
- aigenora/protocols/5cd50a30/977f690c81073e861c9fec9323b3bf359e703c886ff9b0153c1cb209/ui/index.html +186 -0
- aigenora/protocols/6fdb1053/bf4b3c2eb280d0f48215042ce2a330bef4d14113fdab3c4f997a0827/hooks.py +305 -0
- aigenora/protocols/6fdb1053/bf4b3c2eb280d0f48215042ce2a330bef4d14113fdab3c4f997a0827/spec.json +112 -0
- aigenora/protocols/6fdb1053/bf4b3c2eb280d0f48215042ce2a330bef4d14113fdab3c4f997a0827/ui/index.html +235 -0
- aigenora/protocols/83ae1cd7/9f397624990fd280667864667143cdf5c80e8a8923b364dbba710396/hooks.py +285 -0
- aigenora/protocols/83ae1cd7/9f397624990fd280667864667143cdf5c80e8a8923b364dbba710396/spec.json +301 -0
- aigenora/protocols/83ae1cd7/9f397624990fd280667864667143cdf5c80e8a8923b364dbba710396/ui/index.html +334 -0
- aigenora/protocols/9e5df77c/31b613dd16b015ed802d4d063d446c4e381382885683c89b7f5ee48f/hooks.py +203 -0
- aigenora/protocols/9e5df77c/31b613dd16b015ed802d4d063d446c4e381382885683c89b7f5ee48f/spec.json +289 -0
- aigenora/protocols/9e5df77c/31b613dd16b015ed802d4d063d446c4e381382885683c89b7f5ee48f/ui/index.html +306 -0
- aigenora/protocols/b5d235f2/fab5e22983329505a44f3778a06946d6107f1884370e2bb6656fcfe2/hooks.py +262 -0
- aigenora/protocols/b5d235f2/fab5e22983329505a44f3778a06946d6107f1884370e2bb6656fcfe2/spec.json +326 -0
- aigenora/protocols/b5d235f2/fab5e22983329505a44f3778a06946d6107f1884370e2bb6656fcfe2/ui/index.html +307 -0
- aigenora/protocols/index.json +314 -0
- aigenora/protocols/templates/README.md +28 -0
- aigenora/protocols/templates/bidding.json +57 -0
- aigenora/protocols/templates/demand.json +64 -0
- aigenora/protocols/templates/free-chat.json +56 -0
- aigenora/protocols/templates/qna-service.json +65 -0
- aigenora/protocols/templates/request-response.json +63 -0
- aigenora/protocols/templates/simultaneous-bid.json +104 -0
- aigenora/protocols/templates/turn-based-game.json +80 -0
- aigenora/protocols/templates/ui-example/index.html +128 -0
- aigenora/skill/PERSONAL.md +106 -0
- aigenora/skill/SKILL.md +2006 -0
- aigenora/skill/__init__.py +2 -0
- aigenora/skill/protocols/templates/README.md +28 -0
- aigenora/skill/protocols/templates/bidding.json +57 -0
- aigenora/skill/protocols/templates/demand.json +64 -0
- aigenora/skill/protocols/templates/free-chat.json +56 -0
- aigenora/skill/protocols/templates/qna-service.json +65 -0
- aigenora/skill/protocols/templates/request-response.json +63 -0
- aigenora/skill/protocols/templates/simultaneous-bid.json +104 -0
- aigenora/skill/protocols/templates/turn-based-game.json +80 -0
- aigenora-0.0.1.dist-info/METADATA +159 -0
- aigenora-0.0.1.dist-info/RECORD +100 -0
- aigenora-0.0.1.dist-info/WHEEL +5 -0
- aigenora-0.0.1.dist-info/entry_points.txt +2 -0
- aigenora-0.0.1.dist-info/licenses/LICENSE +21 -0
- aigenora-0.0.1.dist-info/top_level.txt +1 -0
aigenora/__init__.py
ADDED
aigenora/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from aigenora.proto.sdk import EventBus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Cold start of the business subprocess (iroh node + spec load + invitation publish) has been
|
|
14
|
+
# observed at ~18-30s on Windows. 15s caused false "timeout waiting for invite_created" while
|
|
15
|
+
# the subprocess was still alive. Override with AIGENORA_DAEMON_STARTUP_TIMEOUT env if needed.
|
|
16
|
+
DEFAULT_STARTUP_WAIT_SECONDS = 30.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def startup_wait_seconds() -> float:
|
|
20
|
+
raw = os.environ.get("AIGENORA_DAEMON_STARTUP_TIMEOUT")
|
|
21
|
+
if raw is None or raw == "":
|
|
22
|
+
return DEFAULT_STARTUP_WAIT_SECONDS
|
|
23
|
+
try:
|
|
24
|
+
return max(0.0, float(raw))
|
|
25
|
+
except ValueError:
|
|
26
|
+
return DEFAULT_STARTUP_WAIT_SECONDS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def wait_for_event(
|
|
30
|
+
state_dir: str | Path,
|
|
31
|
+
event_type: str,
|
|
32
|
+
*,
|
|
33
|
+
timeout_seconds: float | None = None,
|
|
34
|
+
required_data_keys: tuple[str, ...] = (),
|
|
35
|
+
) -> dict[str, Any] | None:
|
|
36
|
+
"""Poll state_dir/events.jsonl until a matching startup event appears."""
|
|
37
|
+
timeout = startup_wait_seconds() if timeout_seconds is None else max(0.0, timeout_seconds)
|
|
38
|
+
deadline = time.monotonic() + timeout
|
|
39
|
+
bus = EventBus(state_dir)
|
|
40
|
+
while True:
|
|
41
|
+
for event in bus.read_events():
|
|
42
|
+
if event.get("type") != event_type:
|
|
43
|
+
continue
|
|
44
|
+
data = event.get("data") or {}
|
|
45
|
+
if all(data.get(k) for k in required_data_keys):
|
|
46
|
+
return event
|
|
47
|
+
if time.monotonic() >= deadline:
|
|
48
|
+
return None
|
|
49
|
+
time.sleep(0.1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def write_session_meta(state_dir: str | Path, meta: dict[str, Any]) -> None:
|
|
53
|
+
Path(state_dir, "session.json").write_text(json.dumps(meta, ensure_ascii=False), encoding="utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def update_session_meta(state_dir: str | Path | None, **updates: Any) -> None:
|
|
57
|
+
"""Read-modify-write session.json.
|
|
58
|
+
|
|
59
|
+
The daemon parent process (host._run_daemon / join._run_daemon) writes the initial
|
|
60
|
+
session.json and returns right after startup, so it never observes how the business
|
|
61
|
+
subprocess ends. The business subprocess therefore calls this on its terminal path to
|
|
62
|
+
record the final status (closed/aborted), ended_at, game_over and end_reason — otherwise
|
|
63
|
+
console/list keeps showing a stale "running" session for a process that already exited.
|
|
64
|
+
Best-effort: a missing/unreadable session.json or a None state_dir is a silent no-op.
|
|
65
|
+
"""
|
|
66
|
+
if not state_dir:
|
|
67
|
+
return
|
|
68
|
+
path = Path(state_dir, "session.json")
|
|
69
|
+
if not path.exists():
|
|
70
|
+
return
|
|
71
|
+
try:
|
|
72
|
+
meta = json.loads(path.read_text(encoding="utf-8"))
|
|
73
|
+
except Exception as e:
|
|
74
|
+
# session.json 损坏/不可读:原为静默 return,导致终态丢失且无从排查。改为记录 warning。
|
|
75
|
+
print(f"[aigenora] warning: failed to read session.json for update: {e}", file=sys.stderr)
|
|
76
|
+
return
|
|
77
|
+
meta.update(updates)
|
|
78
|
+
path.write_text(json.dumps(meta, ensure_ascii=False), encoding="utf-8")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def read_log_excerpt(state_dir: str | Path, name: str = "daemon.err.log", limit: int = 500) -> str:
|
|
82
|
+
path = Path(state_dir) / name
|
|
83
|
+
if not path.exists() or path.stat().st_size <= 0:
|
|
84
|
+
return ""
|
|
85
|
+
with path.open("rb") as f:
|
|
86
|
+
size = path.stat().st_size
|
|
87
|
+
if size > limit:
|
|
88
|
+
f.seek(size - limit)
|
|
89
|
+
return f.read().decode("utf-8", errors="replace")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def terminate_process(proc: Any) -> None:
|
|
93
|
+
try:
|
|
94
|
+
proc.terminate()
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Web UI auto-launch mode resolution.
|
|
2
|
+
|
|
3
|
+
Three modes:
|
|
4
|
+
- auto : start the relay subprocess and open the browser automatically (default behavior)
|
|
5
|
+
- headless: start the relay subprocess without opening a browser (print the URL for the user to open manually)
|
|
6
|
+
- off : do not start the relay subprocess (pure CLI)
|
|
7
|
+
|
|
8
|
+
Priority (high -> low):
|
|
9
|
+
1. CLI argument: --web {auto,headless,off} (mutually-exclusive aliases of --no-web / --no-browser)
|
|
10
|
+
2. Environment variable: AIGENORA_WEB
|
|
11
|
+
3. Default value: auto
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
WebMode = Literal["auto", "headless", "off"]
|
|
19
|
+
VALID_MODES: tuple[WebMode, ...] = ("auto", "headless", "off")
|
|
20
|
+
DEFAULT_MODE: WebMode = "auto"
|
|
21
|
+
ENV_VAR = "AIGENORA_WEB"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize(value: str | None) -> WebMode | None:
|
|
25
|
+
"""Normalize to a valid WebMode; return None if invalid or empty."""
|
|
26
|
+
if value is None:
|
|
27
|
+
return None
|
|
28
|
+
v = value.strip().lower()
|
|
29
|
+
if v in VALID_MODES:
|
|
30
|
+
return v # type: ignore[return-value]
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_web_mode(args) -> WebMode:
|
|
35
|
+
"""Resolve the final web mode by CLI > env > default.
|
|
36
|
+
|
|
37
|
+
args is expected to come from argparse and may contain the following optional attributes:
|
|
38
|
+
- web : explicit --web value (auto/headless/off)
|
|
39
|
+
- no_web : --no-web flag
|
|
40
|
+
- no_browser: --no-browser flag
|
|
41
|
+
"""
|
|
42
|
+
explicit = normalize(getattr(args, "web", None))
|
|
43
|
+
if explicit is not None:
|
|
44
|
+
return explicit
|
|
45
|
+
if getattr(args, "no_web", False):
|
|
46
|
+
return "off"
|
|
47
|
+
if getattr(args, "no_browser", False):
|
|
48
|
+
return "headless"
|
|
49
|
+
env = normalize(os.environ.get(ENV_VAR))
|
|
50
|
+
if env is not None:
|
|
51
|
+
return env
|
|
52
|
+
return DEFAULT_MODE
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""aigenora bootstrap: one-shot environment probe for user agents.
|
|
2
|
+
|
|
3
|
+
Design principles (must be followed):
|
|
4
|
+
- Diagnosis only, never auto-fix. Repair suggestions are returned as strings; the
|
|
5
|
+
caller (human/agent) decides whether to act on them.
|
|
6
|
+
- Output is both machine-parseable (--json) and human-readable.
|
|
7
|
+
- No network dependency, no community-server dependency.
|
|
8
|
+
- Fields are stable; new fields must be backward compatible; do not rename published fields.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import platform as pyplatform
|
|
17
|
+
import shutil
|
|
18
|
+
import sys
|
|
19
|
+
import sysconfig
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from aigenora import __version__ as PKG_VERSION
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
REQUIRED_DEPS = ("cryptography", "httpx", "iroh")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _platform_id() -> str:
|
|
29
|
+
sysname = sys.platform
|
|
30
|
+
if sysname.startswith("win"):
|
|
31
|
+
return "windows"
|
|
32
|
+
if sysname == "darwin":
|
|
33
|
+
return "macos"
|
|
34
|
+
return "linux"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check_skill() -> tuple[str | None, str | None, str | None]:
|
|
38
|
+
"""Returns (skill_md_path, skill_version, error)."""
|
|
39
|
+
try:
|
|
40
|
+
from importlib import resources
|
|
41
|
+
from aigenora.agent.skill import _extract_skill_version
|
|
42
|
+
|
|
43
|
+
res = resources.files("aigenora.skill").joinpath("SKILL.md")
|
|
44
|
+
text = res.read_text(encoding="utf-8-sig")
|
|
45
|
+
ver = _extract_skill_version(text)
|
|
46
|
+
return (str(res), ver, None if ver else "missing 'version:' frontmatter")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return (None, None, str(e))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _check_deps() -> list[str]:
|
|
52
|
+
return [m for m in REQUIRED_DEPS if importlib.util.find_spec(m) is None]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _data_dir_default() -> str:
|
|
56
|
+
raw = os.environ.get("P2P_DATA_DIR") or os.environ.get("AGENT_DIR")
|
|
57
|
+
if raw:
|
|
58
|
+
return str(Path(raw).expanduser())
|
|
59
|
+
return str(Path.cwd() / ".aigenora")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def collect() -> dict:
|
|
63
|
+
"""Collect environment probe data."""
|
|
64
|
+
scripts_dir = sysconfig.get_paths().get("scripts", "")
|
|
65
|
+
cmd_path = shutil.which("aigenora")
|
|
66
|
+
in_path = cmd_path is not None
|
|
67
|
+
|
|
68
|
+
skill_path, skill_ver, skill_err = _check_skill()
|
|
69
|
+
missing_deps = _check_deps()
|
|
70
|
+
|
|
71
|
+
issues: list[dict] = []
|
|
72
|
+
|
|
73
|
+
if missing_deps:
|
|
74
|
+
issues.append({
|
|
75
|
+
"code": "DEPS_MISSING",
|
|
76
|
+
"message": f"missing python packages: {', '.join(missing_deps)}",
|
|
77
|
+
"fix": "ask user to run: pip install aigenora-client",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if skill_err and not skill_path:
|
|
81
|
+
issues.append({
|
|
82
|
+
"code": "SKILL_NOT_PACKAGED",
|
|
83
|
+
"message": f"packaged SKILL.md unavailable: {skill_err}",
|
|
84
|
+
"fix": "reinstall aigenora-client; the package data may be corrupt",
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if not in_path:
|
|
88
|
+
issues.append({
|
|
89
|
+
"code": "CMD_NOT_IN_PATH",
|
|
90
|
+
"message": "the `aigenora` console script is not in PATH",
|
|
91
|
+
"fix": f"use `{sys.executable} -m aigenora ...` (recommended); "
|
|
92
|
+
f"or add to PATH: {scripts_dir}",
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
data = {
|
|
96
|
+
"ok": all(i["code"] not in ("DEPS_MISSING", "SKILL_NOT_PACKAGED") for i in issues),
|
|
97
|
+
"version": PKG_VERSION,
|
|
98
|
+
"python": sys.executable,
|
|
99
|
+
"python_version": pyplatform.python_version(),
|
|
100
|
+
"platform": _platform_id(),
|
|
101
|
+
"recommended_entrypoint": f"{sys.executable} -m aigenora",
|
|
102
|
+
"console_script_in_path": in_path,
|
|
103
|
+
"console_script_path": cmd_path,
|
|
104
|
+
"console_script_dir": scripts_dir,
|
|
105
|
+
"skill_md_path": skill_path,
|
|
106
|
+
"skill_version": skill_ver,
|
|
107
|
+
"data_dir_default": _data_dir_default(),
|
|
108
|
+
"issues": issues,
|
|
109
|
+
}
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _print_human(data: dict) -> None:
|
|
114
|
+
print(f"ok: {data['ok']}")
|
|
115
|
+
print(f"version: {data['version']}")
|
|
116
|
+
print(f"python: {data['python']} ({data['python_version']})")
|
|
117
|
+
print(f"platform: {data['platform']}")
|
|
118
|
+
print(f"recommended entrypoint: {data['recommended_entrypoint']}")
|
|
119
|
+
if data["console_script_in_path"]:
|
|
120
|
+
print(f"aigenora cmd: {data['console_script_path']}")
|
|
121
|
+
else:
|
|
122
|
+
print(f"aigenora cmd: NOT IN PATH")
|
|
123
|
+
print(f" scripts dir: {data['console_script_dir']}")
|
|
124
|
+
print(f"skill md: {data['skill_md_path']} (version={data['skill_version']})")
|
|
125
|
+
print(f"data dir default: {data['data_dir_default']}")
|
|
126
|
+
if data["issues"]:
|
|
127
|
+
print("issues:")
|
|
128
|
+
for i in data["issues"]:
|
|
129
|
+
print(f" [{i['code']}] {i['message']}")
|
|
130
|
+
print(f" fix: {i['fix']}")
|
|
131
|
+
else:
|
|
132
|
+
print("issues: (none)")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run(args) -> int:
|
|
136
|
+
data = collect()
|
|
137
|
+
if getattr(args, "json_output", False):
|
|
138
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
139
|
+
else:
|
|
140
|
+
_print_human(data)
|
|
141
|
+
return 0 if data["ok"] else 1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_subparser(parent_sub) -> argparse.ArgumentParser:
|
|
145
|
+
bs = parent_sub.add_parser(
|
|
146
|
+
"bootstrap",
|
|
147
|
+
help="One-shot environment probe: returns python/package/skill/PATH status (agent-friendly)",
|
|
148
|
+
)
|
|
149
|
+
bs.add_argument("--json", action="store_true", dest="json_output",
|
|
150
|
+
help="Output machine-parseable JSON")
|
|
151
|
+
return bs
|
aigenora/agent/browse.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
from aigenora.engine.config import get_server
|
|
7
|
+
from aigenora.engine.keys import load_keys
|
|
8
|
+
from aigenora.engine.rest import RestClient
|
|
9
|
+
|
|
10
|
+
_PROTOCOL_ID_RE = re.compile(r"[0-9a-f]{64}")
|
|
11
|
+
_TAG_RE = re.compile(r"[A-Za-z0-9_.:-]+")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _pricing_text(item) -> str:
|
|
15
|
+
if not isinstance(item, dict):
|
|
16
|
+
return ""
|
|
17
|
+
options = item.get("options")
|
|
18
|
+
if isinstance(options, dict):
|
|
19
|
+
pricing = options.get("pricing")
|
|
20
|
+
else:
|
|
21
|
+
pricing = item.get("pricing")
|
|
22
|
+
if not isinstance(pricing, dict):
|
|
23
|
+
return ""
|
|
24
|
+
model = pricing.get("model", "")
|
|
25
|
+
if model == "free":
|
|
26
|
+
return "free"
|
|
27
|
+
amount = pricing.get("amount", "")
|
|
28
|
+
currency = pricing.get("currency", "")
|
|
29
|
+
return f"{amount} {currency}".strip() or model
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_tags_filter(tags_arg: str | None) -> list[str]:
|
|
33
|
+
if tags_arg is None:
|
|
34
|
+
return []
|
|
35
|
+
if not tags_arg:
|
|
36
|
+
raise ValueError("--tags cannot be empty")
|
|
37
|
+
tags: list[str] = []
|
|
38
|
+
for raw in tags_arg.split(","):
|
|
39
|
+
tag = raw.strip()
|
|
40
|
+
if not tag:
|
|
41
|
+
continue
|
|
42
|
+
if len(tag) > 64:
|
|
43
|
+
raise ValueError("--tags entries must be at most 64 characters")
|
|
44
|
+
if not _TAG_RE.fullmatch(tag):
|
|
45
|
+
raise ValueError("--tags entries may contain only A-Za-z0-9_.:-")
|
|
46
|
+
if tag not in tags:
|
|
47
|
+
tags.append(tag)
|
|
48
|
+
if len(tags) > 10:
|
|
49
|
+
raise ValueError("--tags accepts at most 10 tags")
|
|
50
|
+
if not tags:
|
|
51
|
+
raise ValueError("--tags cannot be empty")
|
|
52
|
+
return tags
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_filters(args) -> None:
|
|
56
|
+
if getattr(args, "post_id", None):
|
|
57
|
+
return
|
|
58
|
+
_validate_tags_filter(getattr(args, "tags", None))
|
|
59
|
+
protocol_id = getattr(args, "protocol_id", None)
|
|
60
|
+
if protocol_id is not None:
|
|
61
|
+
if not protocol_id:
|
|
62
|
+
raise ValueError("--protocol-id cannot be empty")
|
|
63
|
+
if not _PROTOCOL_ID_RE.fullmatch(protocol_id):
|
|
64
|
+
raise ValueError("--protocol-id must be a 64-char lowercase protocol hash")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run(args) -> int:
|
|
68
|
+
try:
|
|
69
|
+
_validate_filters(args)
|
|
70
|
+
except ValueError as exc:
|
|
71
|
+
print(f"error: {exc}")
|
|
72
|
+
return 2
|
|
73
|
+
|
|
74
|
+
kp = load_keys(args.data_dir)
|
|
75
|
+
server = get_server(args.server)
|
|
76
|
+
client = RestClient(server, kp)
|
|
77
|
+
if args.post_id:
|
|
78
|
+
data = client.json("GET", f"/api/v1/invitations/{args.post_id}", expected={200})
|
|
79
|
+
items = [data]
|
|
80
|
+
total = 1
|
|
81
|
+
else:
|
|
82
|
+
query = {k: v for k, v in {
|
|
83
|
+
"tags": args.tags,
|
|
84
|
+
"protocol_id": args.protocol_id,
|
|
85
|
+
"type": args.type,
|
|
86
|
+
"limit": args.limit,
|
|
87
|
+
}.items() if v is not None}
|
|
88
|
+
path = "/api/v1/invitations"
|
|
89
|
+
if query:
|
|
90
|
+
path += "?" + urlencode(query)
|
|
91
|
+
data = client.json("GET", path, expected={200})
|
|
92
|
+
items = data.get("results", data if isinstance(data, list) else [])
|
|
93
|
+
total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
|
|
94
|
+
if args.oneline:
|
|
95
|
+
for item in items:
|
|
96
|
+
tags = item.get("tags", [])
|
|
97
|
+
if isinstance(tags, list):
|
|
98
|
+
tags_text = ",".join(str(t) for t in tags)
|
|
99
|
+
else:
|
|
100
|
+
tags_text = str(tags or "")
|
|
101
|
+
print("\t".join([
|
|
102
|
+
str(item.get("post_id", "")),
|
|
103
|
+
str(item.get("protocol_id", "") or ""),
|
|
104
|
+
str(item.get("type", "") or "chat"),
|
|
105
|
+
str(item.get("message", "") or ""),
|
|
106
|
+
tags_text,
|
|
107
|
+
str(item.get("public_key", "") or ""),
|
|
108
|
+
"true" if item.get("registered", False) else "false",
|
|
109
|
+
str(item.get("nickname", "") or ""),
|
|
110
|
+
str(item.get("agent_id", "") or ""),
|
|
111
|
+
_pricing_text(item),
|
|
112
|
+
]))
|
|
113
|
+
return 0
|
|
114
|
+
print(f"Total: {total}")
|
|
115
|
+
for item in items:
|
|
116
|
+
print(f"{item.get('post_id')} [{item.get('type', 'chat')}] {item.get('message', '')}")
|
|
117
|
+
print(f" protocol_id: {item.get('protocol_id', '')}")
|
|
118
|
+
print(f" public_key: {item.get('public_key', '')}")
|
|
119
|
+
return 0
|
|
120
|
+
|
aigenora/agent/cancel.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from aigenora.engine.config import get_server
|
|
4
|
+
from aigenora.engine.keys import load_keys
|
|
5
|
+
from aigenora.engine.rest import RestClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(args) -> int:
|
|
9
|
+
kp = load_keys(args.data_dir)
|
|
10
|
+
client = RestClient(get_server(args.server), kp)
|
|
11
|
+
client.json("DELETE", f"/api/v1/invitations/{args.post_id}", expected={204})
|
|
12
|
+
print("[OK] invitation cancelled")
|
|
13
|
+
return 0
|
|
14
|
+
|