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.
Files changed (100) hide show
  1. aigenora/__init__.py +2 -0
  2. aigenora/__main__.py +6 -0
  3. aigenora/agent/__init__.py +1 -0
  4. aigenora/agent/_daemon.py +96 -0
  5. aigenora/agent/_web_mode.py +52 -0
  6. aigenora/agent/bootstrap.py +151 -0
  7. aigenora/agent/browse.py +120 -0
  8. aigenora/agent/cancel.py +14 -0
  9. aigenora/agent/console.py +265 -0
  10. aigenora/agent/doctor.py +26 -0
  11. aigenora/agent/elo.py +63 -0
  12. aigenora/agent/feedback.py +34 -0
  13. aigenora/agent/guest.py +29 -0
  14. aigenora/agent/host.py +347 -0
  15. aigenora/agent/inbox.py +162 -0
  16. aigenora/agent/init.py +108 -0
  17. aigenora/agent/join.py +272 -0
  18. aigenora/agent/karma.py +70 -0
  19. aigenora/agent/protocol.py +509 -0
  20. aigenora/agent/protocol_adversarial.py +214 -0
  21. aigenora/agent/protocol_preflight.py +182 -0
  22. aigenora/agent/protocol_search.py +217 -0
  23. aigenora/agent/protocol_ui.py +243 -0
  24. aigenora/agent/register.py +30 -0
  25. aigenora/agent/registry.py +76 -0
  26. aigenora/agent/session.py +594 -0
  27. aigenora/agent/skeleton.py +538 -0
  28. aigenora/agent/skill.py +391 -0
  29. aigenora/agent/trust.py +221 -0
  30. aigenora/agent/validate.py +17 -0
  31. aigenora/agent/web.py +1621 -0
  32. aigenora/cli.py +558 -0
  33. aigenora/engine/__init__.py +1 -0
  34. aigenora/engine/box.py +95 -0
  35. aigenora/engine/config.py +98 -0
  36. aigenora/engine/crypto.py +139 -0
  37. aigenora/engine/keys.py +100 -0
  38. aigenora/engine/p2p.py +418 -0
  39. aigenora/engine/rest.py +34 -0
  40. aigenora/proto/__init__.py +1 -0
  41. aigenora/proto/decide_gateway.py +122 -0
  42. aigenora/proto/engine.py +1761 -0
  43. aigenora/proto/hooks.py +133 -0
  44. aigenora/proto/loader.py +28 -0
  45. aigenora/proto/prefs.py +142 -0
  46. aigenora/proto/sdk.py +685 -0
  47. aigenora/proto/session.py +115 -0
  48. aigenora/proto/spec_version.py +29 -0
  49. aigenora/proto/validate.py +271 -0
  50. aigenora/protocols/166570ef/f5c0864d31ccafb9d04ea5154184542085dfa401a9c3590f6831e8c8/hooks.py +227 -0
  51. aigenora/protocols/166570ef/f5c0864d31ccafb9d04ea5154184542085dfa401a9c3590f6831e8c8/spec.json +237 -0
  52. aigenora/protocols/166570ef/f5c0864d31ccafb9d04ea5154184542085dfa401a9c3590f6831e8c8/ui/index.html +317 -0
  53. aigenora/protocols/5358130e/14885f61ff210d2dd44c58a15ca4140bd89558f41f3eb867bc188e8f/hooks.py +342 -0
  54. aigenora/protocols/5358130e/14885f61ff210d2dd44c58a15ca4140bd89558f41f3eb867bc188e8f/spec.json +126 -0
  55. aigenora/protocols/5358130e/14885f61ff210d2dd44c58a15ca4140bd89558f41f3eb867bc188e8f/ui/index.html +192 -0
  56. aigenora/protocols/59da01bc/80d2ac385e0c9c642ad578ffa83b8fe273c4df21d80b2555437b8a31/hooks.py +83 -0
  57. aigenora/protocols/59da01bc/80d2ac385e0c9c642ad578ffa83b8fe273c4df21d80b2555437b8a31/spec.json +52 -0
  58. aigenora/protocols/59da01bc/80d2ac385e0c9c642ad578ffa83b8fe273c4df21d80b2555437b8a31/ui/index.html +165 -0
  59. aigenora/protocols/5cd50a30/977f690c81073e861c9fec9323b3bf359e703c886ff9b0153c1cb209/hooks.py +292 -0
  60. aigenora/protocols/5cd50a30/977f690c81073e861c9fec9323b3bf359e703c886ff9b0153c1cb209/spec.json +114 -0
  61. aigenora/protocols/5cd50a30/977f690c81073e861c9fec9323b3bf359e703c886ff9b0153c1cb209/ui/index.html +186 -0
  62. aigenora/protocols/6fdb1053/bf4b3c2eb280d0f48215042ce2a330bef4d14113fdab3c4f997a0827/hooks.py +305 -0
  63. aigenora/protocols/6fdb1053/bf4b3c2eb280d0f48215042ce2a330bef4d14113fdab3c4f997a0827/spec.json +112 -0
  64. aigenora/protocols/6fdb1053/bf4b3c2eb280d0f48215042ce2a330bef4d14113fdab3c4f997a0827/ui/index.html +235 -0
  65. aigenora/protocols/83ae1cd7/9f397624990fd280667864667143cdf5c80e8a8923b364dbba710396/hooks.py +285 -0
  66. aigenora/protocols/83ae1cd7/9f397624990fd280667864667143cdf5c80e8a8923b364dbba710396/spec.json +301 -0
  67. aigenora/protocols/83ae1cd7/9f397624990fd280667864667143cdf5c80e8a8923b364dbba710396/ui/index.html +334 -0
  68. aigenora/protocols/9e5df77c/31b613dd16b015ed802d4d063d446c4e381382885683c89b7f5ee48f/hooks.py +203 -0
  69. aigenora/protocols/9e5df77c/31b613dd16b015ed802d4d063d446c4e381382885683c89b7f5ee48f/spec.json +289 -0
  70. aigenora/protocols/9e5df77c/31b613dd16b015ed802d4d063d446c4e381382885683c89b7f5ee48f/ui/index.html +306 -0
  71. aigenora/protocols/b5d235f2/fab5e22983329505a44f3778a06946d6107f1884370e2bb6656fcfe2/hooks.py +262 -0
  72. aigenora/protocols/b5d235f2/fab5e22983329505a44f3778a06946d6107f1884370e2bb6656fcfe2/spec.json +326 -0
  73. aigenora/protocols/b5d235f2/fab5e22983329505a44f3778a06946d6107f1884370e2bb6656fcfe2/ui/index.html +307 -0
  74. aigenora/protocols/index.json +314 -0
  75. aigenora/protocols/templates/README.md +28 -0
  76. aigenora/protocols/templates/bidding.json +57 -0
  77. aigenora/protocols/templates/demand.json +64 -0
  78. aigenora/protocols/templates/free-chat.json +56 -0
  79. aigenora/protocols/templates/qna-service.json +65 -0
  80. aigenora/protocols/templates/request-response.json +63 -0
  81. aigenora/protocols/templates/simultaneous-bid.json +104 -0
  82. aigenora/protocols/templates/turn-based-game.json +80 -0
  83. aigenora/protocols/templates/ui-example/index.html +128 -0
  84. aigenora/skill/PERSONAL.md +106 -0
  85. aigenora/skill/SKILL.md +2006 -0
  86. aigenora/skill/__init__.py +2 -0
  87. aigenora/skill/protocols/templates/README.md +28 -0
  88. aigenora/skill/protocols/templates/bidding.json +57 -0
  89. aigenora/skill/protocols/templates/demand.json +64 -0
  90. aigenora/skill/protocols/templates/free-chat.json +56 -0
  91. aigenora/skill/protocols/templates/qna-service.json +65 -0
  92. aigenora/skill/protocols/templates/request-response.json +63 -0
  93. aigenora/skill/protocols/templates/simultaneous-bid.json +104 -0
  94. aigenora/skill/protocols/templates/turn-based-game.json +80 -0
  95. aigenora-0.0.1.dist-info/METADATA +159 -0
  96. aigenora-0.0.1.dist-info/RECORD +100 -0
  97. aigenora-0.0.1.dist-info/WHEEL +5 -0
  98. aigenora-0.0.1.dist-info/entry_points.txt +2 -0
  99. aigenora-0.0.1.dist-info/licenses/LICENSE +21 -0
  100. aigenora-0.0.1.dist-info/top_level.txt +1 -0
aigenora/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+
aigenora/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
6
+
@@ -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
@@ -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
+
@@ -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
+