forgexa-cli 1.13.2__tar.gz → 1.13.3__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.
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/PKG-INFO +1 -1
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli/daemon.py +4 -2
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli/main.py +384 -34
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/pyproject.toml +1 -1
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/README.md +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/setup.cfg +0 -0
- {forgexa_cli-1.13.2 → forgexa_cli-1.13.3}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.13.
|
|
2
|
+
__version__ = "1.13.3"
|
|
@@ -523,7 +523,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
523
523
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
524
524
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
525
525
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
526
|
-
DAEMON_VERSION = "1.13.
|
|
526
|
+
DAEMON_VERSION = "1.13.3"
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def _detect_client_type() -> str:
|
|
@@ -555,6 +555,8 @@ _CLIENT_TYPE = _detect_client_type()
|
|
|
555
555
|
# (Makefile, Tauri desktop app, `forgexa daemon start --detach`, etc.).
|
|
556
556
|
# A StreamHandler is added only when stderr is a TTY (foreground mode)
|
|
557
557
|
# so logs are visible on the terminal without duplication.
|
|
558
|
+
# Set DAEMON_NO_CONSOLE_LOG=1 to suppress the stream handler even on a TTY
|
|
559
|
+
# (used by `forgexa daemon start` foreground mode for clean UX).
|
|
558
560
|
_log_dir = Path.home() / ".forgexa" / "daemon"
|
|
559
561
|
_log_dir.mkdir(parents=True, exist_ok=True)
|
|
560
562
|
DAEMON_LOG_PATH = _log_dir / "daemon.log"
|
|
@@ -566,7 +568,7 @@ _log_handlers: list[logging.Handler] = [
|
|
|
566
568
|
backupCount=5,
|
|
567
569
|
),
|
|
568
570
|
]
|
|
569
|
-
if sys.stderr.isatty():
|
|
571
|
+
if sys.stderr.isatty() and not os.environ.get("DAEMON_NO_CONSOLE_LOG"):
|
|
570
572
|
_log_handlers.append(logging.StreamHandler(sys.stderr))
|
|
571
573
|
|
|
572
574
|
logging.basicConfig(
|
|
@@ -16,7 +16,14 @@ Usage:
|
|
|
16
16
|
forgexa workspace list
|
|
17
17
|
forgexa project list --workspace <id>
|
|
18
18
|
forgexa requirement list --project <id>
|
|
19
|
+
forgexa daemon start -d
|
|
19
20
|
forgexa daemon status
|
|
21
|
+
forgexa daemon status --verbose
|
|
22
|
+
forgexa daemon logs -f
|
|
23
|
+
forgexa daemon logs -n 100 -f
|
|
24
|
+
forgexa daemon agents
|
|
25
|
+
forgexa daemon restart
|
|
26
|
+
forgexa daemon stop
|
|
20
27
|
forgexa gates pending
|
|
21
28
|
forgexa config show
|
|
22
29
|
forgexa --help
|
|
@@ -645,26 +652,185 @@ def _get_runtimes(include_all: bool = False) -> list[dict]:
|
|
|
645
652
|
return runtimes if isinstance(runtimes, list) else []
|
|
646
653
|
|
|
647
654
|
|
|
655
|
+
def _get_runtimes_silent(include_all: bool = False) -> list[dict]:
|
|
656
|
+
"""Like _get_runtimes() but returns [] instead of sys.exit on any error."""
|
|
657
|
+
import urllib.request
|
|
658
|
+
|
|
659
|
+
path = "/runtimes" if include_all else "/runtimes/me"
|
|
660
|
+
url = f"{_api_url()}/api/v1{path}"
|
|
661
|
+
req = urllib.request.Request(url, headers=_headers(), method="GET")
|
|
662
|
+
try:
|
|
663
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
664
|
+
content = resp.read()
|
|
665
|
+
result = json.loads(content) if content else []
|
|
666
|
+
return result if isinstance(result, list) else []
|
|
667
|
+
except Exception:
|
|
668
|
+
return []
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _scan_local_agents() -> list[dict]:
|
|
672
|
+
"""Run a quick local agent discovery scan and return results as dicts.
|
|
673
|
+
|
|
674
|
+
Uses asyncio.run() so it works from synchronous CLI context.
|
|
675
|
+
Silently returns [] if anything goes wrong.
|
|
676
|
+
"""
|
|
677
|
+
try:
|
|
678
|
+
import asyncio as _asyncio
|
|
679
|
+
from forgexa_cli.daemon import AgentDiscovery
|
|
680
|
+
discovered = _asyncio.run(AgentDiscovery().discover())
|
|
681
|
+
return [
|
|
682
|
+
{
|
|
683
|
+
"agent_id": a.agent_id,
|
|
684
|
+
"version": a.version,
|
|
685
|
+
"compatibility_level": a.compatibility_level,
|
|
686
|
+
}
|
|
687
|
+
for a in discovered
|
|
688
|
+
]
|
|
689
|
+
except Exception:
|
|
690
|
+
return []
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _show_discovered_agents(agents: list[dict], indent: str = " ") -> None:
|
|
694
|
+
"""Print a friendly list of discovered agents."""
|
|
695
|
+
if not agents:
|
|
696
|
+
print(f"{indent}No agent CLIs found.")
|
|
697
|
+
print(f"{indent}Install Claude Code, OpenAI Codex, Gemini CLI, Kimi Code, etc.")
|
|
698
|
+
return
|
|
699
|
+
print(f"{indent}{len(agents)} agent(s) discovered:")
|
|
700
|
+
for a in agents:
|
|
701
|
+
agent_id = a.get("agent_id", "?") if isinstance(a, dict) else str(a)
|
|
702
|
+
version = a.get("version", "") if isinstance(a, dict) else ""
|
|
703
|
+
compat = a.get("compatibility_level", "") if isinstance(a, dict) else ""
|
|
704
|
+
badge = f" [{compat}]" if compat else ""
|
|
705
|
+
ver_str = f" {version}" if version else ""
|
|
706
|
+
print(f"{indent} ✓ {agent_id}{ver_str}{badge}")
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _wait_and_show_daemon_agents(start_ts: float, timeout: float = 12.0) -> bool:
|
|
710
|
+
"""Poll server until daemon registers (heartbeat >= start_ts), then show agents.
|
|
711
|
+
|
|
712
|
+
Returns True if registration was confirmed, False on timeout.
|
|
713
|
+
Silently skips if user is not authenticated.
|
|
714
|
+
"""
|
|
715
|
+
if not _token():
|
|
716
|
+
return False
|
|
717
|
+
|
|
718
|
+
from datetime import datetime, timezone as _tz
|
|
719
|
+
|
|
720
|
+
deadline = time.time() + timeout
|
|
721
|
+
spin = ["-", "\\", "|", "/"]
|
|
722
|
+
i = 0
|
|
723
|
+
|
|
724
|
+
sys.stdout.write(" Registering with server...")
|
|
725
|
+
sys.stdout.flush()
|
|
726
|
+
|
|
727
|
+
while time.time() < deadline:
|
|
728
|
+
runtimes = _get_runtimes_silent()
|
|
729
|
+
for r in runtimes:
|
|
730
|
+
hb = r.get("last_heartbeat_at") or ""
|
|
731
|
+
if not hb:
|
|
732
|
+
continue
|
|
733
|
+
try:
|
|
734
|
+
dt = datetime.fromisoformat(hb.replace("Z", "+00:00"))
|
|
735
|
+
hb_ts = dt.timestamp()
|
|
736
|
+
except Exception:
|
|
737
|
+
continue
|
|
738
|
+
# Allow 5-second clock-skew window before the recorded start timestamp
|
|
739
|
+
if hb_ts >= (start_ts - 5):
|
|
740
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
741
|
+
sys.stdout.flush()
|
|
742
|
+
agents = r.get("available_agents", [])
|
|
743
|
+
status = r.get("status", "")
|
|
744
|
+
runtime_short = r["id"][:8]
|
|
745
|
+
print(f" Runtime ready (id: {runtime_short} status: {status})")
|
|
746
|
+
_show_discovered_agents(agents)
|
|
747
|
+
return True
|
|
748
|
+
spin_char = spin[i % 4]
|
|
749
|
+
sys.stdout.write(f"\r {spin_char} Registering with server...")
|
|
750
|
+
sys.stdout.flush()
|
|
751
|
+
i += 1
|
|
752
|
+
time.sleep(0.4)
|
|
753
|
+
|
|
754
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
755
|
+
sys.stdout.flush()
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
|
|
648
759
|
def cmd_daemon_status(args: argparse.Namespace) -> None:
|
|
760
|
+
# ── Local process status ─────────────────────────────────────────────────
|
|
761
|
+
local_pid = _find_running_daemon_pid()
|
|
762
|
+
log_path = Path.home() / ".forgexa" / "daemon" / "daemon.log"
|
|
763
|
+
if local_pid:
|
|
764
|
+
print(f"Local daemon : running (PID {local_pid})")
|
|
765
|
+
else:
|
|
766
|
+
print("Local daemon : not running")
|
|
767
|
+
print(f"Log file : {log_path}")
|
|
768
|
+
print()
|
|
769
|
+
|
|
770
|
+
# ── Server-registered runtimes ───────────────────────────────────────────
|
|
649
771
|
runtimes = _get_runtimes(include_all=getattr(args, "all", False))
|
|
650
772
|
if not runtimes:
|
|
651
|
-
print("No
|
|
773
|
+
print("No runtimes registered on server.")
|
|
774
|
+
if local_pid:
|
|
775
|
+
print("(Daemon is running locally but has not registered yet — wait a moment)")
|
|
652
776
|
return
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
777
|
+
|
|
778
|
+
verbose = getattr(args, "verbose", False)
|
|
779
|
+
|
|
780
|
+
if verbose:
|
|
781
|
+
# Expanded per-runtime block
|
|
782
|
+
for r in runtimes:
|
|
783
|
+
agents = r.get("available_agents", []) or []
|
|
784
|
+
hb = r.get("last_heartbeat_at", "")[:19] if r.get("last_heartbeat_at") else "never"
|
|
785
|
+
print(f"Runtime : {r['id'][:8]} ({r.get('status', '?')})")
|
|
786
|
+
print(f" Daemon : {r.get('daemon_id', '')}")
|
|
787
|
+
print(f" Device : {r.get('device_name', '')}")
|
|
788
|
+
print(f" Active : {r.get('active_tasks', 0)}/{r.get('max_concurrent_tasks', 0)} tasks")
|
|
789
|
+
print(f" Heartbeat : {hb}")
|
|
790
|
+
if agents:
|
|
791
|
+
print(f" Agents :")
|
|
792
|
+
for a in agents:
|
|
793
|
+
if isinstance(a, dict):
|
|
794
|
+
aid = a.get("agent_id", "?")
|
|
795
|
+
ver = a.get("version", "")
|
|
796
|
+
compat = a.get("compatibility_level", "")
|
|
797
|
+
else:
|
|
798
|
+
aid, ver, compat = str(a), "", ""
|
|
799
|
+
badge = f" [{compat}]" if compat else ""
|
|
800
|
+
ver_str = f" {ver}" if ver else ""
|
|
801
|
+
print(f" ✓ {aid}{ver_str}{badge}")
|
|
802
|
+
else:
|
|
803
|
+
print(f" Agents : (none found)")
|
|
804
|
+
print()
|
|
805
|
+
else:
|
|
806
|
+
# Compact table with Agents column
|
|
807
|
+
def _fmt_agents(r: dict) -> str:
|
|
808
|
+
agents = r.get("available_agents", []) or []
|
|
809
|
+
if not agents:
|
|
810
|
+
return "(none)"
|
|
811
|
+
names = [
|
|
812
|
+
a.get("agent_id", "?") if isinstance(a, dict) else str(a)
|
|
813
|
+
for a in agents
|
|
664
814
|
]
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
815
|
+
return ", ".join(names)
|
|
816
|
+
|
|
817
|
+
_print_table(
|
|
818
|
+
["ID", "Daemon", "Device", "Status", "Agents", "Active/Max", "Heartbeat"],
|
|
819
|
+
[
|
|
820
|
+
[
|
|
821
|
+
r["id"][:8],
|
|
822
|
+
r.get("daemon_id", ""),
|
|
823
|
+
r.get("device_name", ""),
|
|
824
|
+
r["status"],
|
|
825
|
+
_fmt_agents(r),
|
|
826
|
+
f"{r.get('active_tasks', 0)}/{r.get('max_concurrent_tasks', 0)}",
|
|
827
|
+
r.get("last_heartbeat_at", "")[:19] if r.get("last_heartbeat_at") else "never",
|
|
828
|
+
]
|
|
829
|
+
for r in runtimes
|
|
830
|
+
],
|
|
831
|
+
)
|
|
832
|
+
print()
|
|
833
|
+
print("Tip: use --verbose for per-agent detail")
|
|
668
834
|
|
|
669
835
|
|
|
670
836
|
def cmd_daemon_stop(_args: argparse.Namespace) -> None:
|
|
@@ -688,49 +854,189 @@ def cmd_daemon_stop(_args: argparse.Namespace) -> None:
|
|
|
688
854
|
print(f"Sent SIGTERM to daemon (PID {pid})")
|
|
689
855
|
|
|
690
856
|
|
|
857
|
+
def cmd_daemon_restart(args: argparse.Namespace) -> None:
|
|
858
|
+
"""Stop any running daemon, then start a fresh one in background."""
|
|
859
|
+
pid = _find_running_daemon_pid()
|
|
860
|
+
if pid is not None:
|
|
861
|
+
print(f"Stopping daemon (PID {pid})...")
|
|
862
|
+
try:
|
|
863
|
+
_stop_daemon_by_pid(pid)
|
|
864
|
+
except (PermissionError, RuntimeError) as exc:
|
|
865
|
+
print(f"Error stopping daemon: {exc}", file=sys.stderr)
|
|
866
|
+
sys.exit(1)
|
|
867
|
+
print("Daemon stopped.")
|
|
868
|
+
print()
|
|
869
|
+
else:
|
|
870
|
+
print("No daemon currently running — starting fresh.")
|
|
871
|
+
print()
|
|
872
|
+
|
|
873
|
+
# restart always runs in background so the shell is returned immediately
|
|
874
|
+
start_args = argparse.Namespace(
|
|
875
|
+
detach=True,
|
|
876
|
+
server_url=getattr(args, "server_url", None),
|
|
877
|
+
)
|
|
878
|
+
cmd_daemon_start(start_args)
|
|
879
|
+
|
|
880
|
+
|
|
691
881
|
def cmd_daemon_start(args: argparse.Namespace) -> None:
|
|
692
882
|
"""Start the local daemon (agent runtime)."""
|
|
693
|
-
|
|
883
|
+
# Refuse to start a second instance
|
|
884
|
+
existing_pid = _find_running_daemon_pid()
|
|
885
|
+
if existing_pid is not None:
|
|
886
|
+
print(f"Daemon is already running (PID {existing_pid}).")
|
|
887
|
+
print("Use 'forgexa daemon restart' to restart it.")
|
|
888
|
+
sys.exit(1)
|
|
889
|
+
|
|
890
|
+
server_url = getattr(args, "server_url", None) or _api_url()
|
|
694
891
|
os.environ["DAEMON_SERVER_URL"] = server_url
|
|
695
892
|
|
|
696
|
-
# Pass token if available
|
|
697
893
|
token = _token()
|
|
698
894
|
if token:
|
|
699
895
|
os.environ.setdefault("DAEMON_API_TOKEN", token)
|
|
700
896
|
|
|
701
897
|
if getattr(args, "detach", False):
|
|
702
|
-
# Background mode
|
|
703
|
-
# Redirect stderr to the canonical log file so logs are not lost.
|
|
898
|
+
# ── Background (detach) mode ─────────────────────────────────────────
|
|
704
899
|
import subprocess as sp
|
|
705
900
|
|
|
706
901
|
log_path = Path.home() / ".forgexa" / "daemon" / "daemon.log"
|
|
707
902
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
708
903
|
|
|
904
|
+
start_ts = time.time()
|
|
709
905
|
cmd = [sys.executable, "-m", "forgexa_cli.daemon"]
|
|
710
906
|
with open(log_path, "a", encoding="utf-8") as log_fh:
|
|
711
907
|
popen_kwargs: dict = dict(stdout=sp.DEVNULL, stderr=log_fh)
|
|
712
908
|
if sys.platform == "win32":
|
|
713
|
-
# Windows: use creation flags to detach the process
|
|
714
909
|
popen_kwargs["creationflags"] = (
|
|
715
910
|
sp.CREATE_NEW_PROCESS_GROUP | sp.DETACHED_PROCESS
|
|
716
911
|
)
|
|
717
912
|
else:
|
|
718
913
|
popen_kwargs["start_new_session"] = True
|
|
719
914
|
proc = sp.Popen(cmd, **popen_kwargs)
|
|
915
|
+
|
|
720
916
|
pid_file = Path.home() / ".forgexa-daemon.pid"
|
|
721
917
|
pid_file.write_text(str(proc.pid))
|
|
722
|
-
|
|
723
|
-
print(f"
|
|
724
|
-
print(f"
|
|
725
|
-
print(f"
|
|
918
|
+
|
|
919
|
+
print(f"Daemon starting in background")
|
|
920
|
+
print(f" PID : {proc.pid}")
|
|
921
|
+
print(f" Server : {server_url}")
|
|
922
|
+
print(f" Logs : {log_path}")
|
|
923
|
+
print()
|
|
924
|
+
|
|
925
|
+
detected = _wait_and_show_daemon_agents(start_ts)
|
|
926
|
+
if not detected:
|
|
927
|
+
if _token():
|
|
928
|
+
print(" (Agent registration not confirmed yet — check with: forgexa daemon status)")
|
|
929
|
+
else:
|
|
930
|
+
print(" (Not logged in — run 'forgexa login' to verify agent registration)")
|
|
931
|
+
print()
|
|
932
|
+
print(f" Stop : forgexa daemon stop")
|
|
933
|
+
print(f" Logs : forgexa daemon logs -f")
|
|
934
|
+
|
|
726
935
|
else:
|
|
727
|
-
# Foreground mode
|
|
728
|
-
|
|
729
|
-
|
|
936
|
+
# ── Foreground mode ──────────────────────────────────────────────────
|
|
937
|
+
# Scan agents locally for immediate friendly display, then run the daemon
|
|
938
|
+
# with console log suppressed so raw log lines don't flood the terminal.
|
|
939
|
+
print(f"Starting daemon (server: {server_url})")
|
|
940
|
+
print()
|
|
941
|
+
print(" Scanning for AI agents...")
|
|
942
|
+
agents = _scan_local_agents()
|
|
943
|
+
_show_discovered_agents(agents)
|
|
944
|
+
print()
|
|
945
|
+
print(" Press Ctrl+C to stop. Logs → forgexa daemon logs -f")
|
|
946
|
+
print()
|
|
947
|
+
|
|
948
|
+
# Suppress raw console log output; logs still written to file
|
|
949
|
+
os.environ["DAEMON_NO_CONSOLE_LOG"] = "1"
|
|
730
950
|
from forgexa_cli.daemon import main_sync
|
|
731
951
|
main_sync()
|
|
732
952
|
|
|
733
953
|
|
|
954
|
+
def cmd_daemon_logs(args: argparse.Namespace) -> None:
|
|
955
|
+
"""Show daemon log output; optionally stream new lines like tail -f."""
|
|
956
|
+
log_path = Path.home() / ".forgexa" / "daemon" / "daemon.log"
|
|
957
|
+
|
|
958
|
+
if not log_path.exists():
|
|
959
|
+
print(f"No daemon log found.")
|
|
960
|
+
print(f" Expected : {log_path}")
|
|
961
|
+
print()
|
|
962
|
+
print("Start the daemon first: forgexa daemon start -d")
|
|
963
|
+
return
|
|
964
|
+
|
|
965
|
+
lines = getattr(args, "lines", 50)
|
|
966
|
+
follow = getattr(args, "follow", False)
|
|
967
|
+
|
|
968
|
+
# Print last N lines
|
|
969
|
+
try:
|
|
970
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
971
|
+
all_lines = f.readlines()
|
|
972
|
+
tail = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
973
|
+
for line in tail:
|
|
974
|
+
print(line, end="")
|
|
975
|
+
except OSError as exc:
|
|
976
|
+
print(f"Cannot read log file: {exc}", file=sys.stderr)
|
|
977
|
+
sys.exit(1)
|
|
978
|
+
|
|
979
|
+
if not follow:
|
|
980
|
+
return
|
|
981
|
+
|
|
982
|
+
# Follow / tail-f mode
|
|
983
|
+
print(f"\n--- streaming {log_path} (Ctrl+C to stop) ---\n", flush=True)
|
|
984
|
+
try:
|
|
985
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
986
|
+
f.seek(0, 2) # jump to end
|
|
987
|
+
while True:
|
|
988
|
+
chunk = f.read(65536)
|
|
989
|
+
if chunk:
|
|
990
|
+
print(chunk, end="", flush=True)
|
|
991
|
+
else:
|
|
992
|
+
time.sleep(0.15)
|
|
993
|
+
except KeyboardInterrupt:
|
|
994
|
+
print() # ensure newline after Ctrl+C
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def cmd_daemon_agents(args: argparse.Namespace) -> None:
|
|
998
|
+
"""List all AI agents across every registered daemon runtime."""
|
|
999
|
+
runtimes = _get_runtimes(include_all=getattr(args, "all", False))
|
|
1000
|
+
if not runtimes:
|
|
1001
|
+
print("No daemons registered.")
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
rows: list[list[str]] = []
|
|
1005
|
+
for r in runtimes:
|
|
1006
|
+
runtime_id = r["id"][:8]
|
|
1007
|
+
daemon_id = r.get("daemon_id", "")
|
|
1008
|
+
device = r.get("device_name", "")
|
|
1009
|
+
status = r.get("status", "")
|
|
1010
|
+
agents = r.get("available_agents", []) or []
|
|
1011
|
+
|
|
1012
|
+
if not agents:
|
|
1013
|
+
rows.append([runtime_id, daemon_id, device, status, "(none)", "", ""])
|
|
1014
|
+
else:
|
|
1015
|
+
first = True
|
|
1016
|
+
for a in agents:
|
|
1017
|
+
if isinstance(a, dict):
|
|
1018
|
+
agent_id = a.get("agent_id", "?")
|
|
1019
|
+
version = a.get("version", "")
|
|
1020
|
+
compat = a.get("compatibility_level", "")
|
|
1021
|
+
else:
|
|
1022
|
+
agent_id, version, compat = str(a), "", ""
|
|
1023
|
+
rows.append([
|
|
1024
|
+
runtime_id if first else "",
|
|
1025
|
+
daemon_id if first else "",
|
|
1026
|
+
device if first else "",
|
|
1027
|
+
status if first else "",
|
|
1028
|
+
agent_id,
|
|
1029
|
+
version,
|
|
1030
|
+
compat,
|
|
1031
|
+
])
|
|
1032
|
+
first = False
|
|
1033
|
+
|
|
1034
|
+
_print_table(
|
|
1035
|
+
["Runtime", "Daemon", "Device", "Status", "Agent", "Version", "Compat"],
|
|
1036
|
+
rows,
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
|
|
734
1040
|
def cmd_runtimes_list(args: argparse.Namespace) -> None:
|
|
735
1041
|
runtimes = _get_runtimes(include_all=getattr(args, "all", False))
|
|
736
1042
|
if not runtimes:
|
|
@@ -1033,12 +1339,53 @@ def main() -> None:
|
|
|
1033
1339
|
# daemon (remote API only — use forgexa-daemon for local daemon management)
|
|
1034
1340
|
daemon_p = sub.add_parser("daemon", help="Daemon management")
|
|
1035
1341
|
daemon_sub = daemon_p.add_subparsers(dest="daemon_cmd")
|
|
1036
|
-
|
|
1037
|
-
daemon_start_p
|
|
1342
|
+
|
|
1343
|
+
daemon_start_p = daemon_sub.add_parser(
|
|
1344
|
+
"start",
|
|
1345
|
+
help="Start local daemon (discovers and registers AI agents)",
|
|
1346
|
+
)
|
|
1347
|
+
daemon_start_p.add_argument(
|
|
1348
|
+
"-d", "--detach",
|
|
1349
|
+
action="store_true",
|
|
1350
|
+
help="Run in background (recommended for production use)",
|
|
1351
|
+
)
|
|
1038
1352
|
daemon_start_p.add_argument("--server-url", default=None, help="Server URL to connect to")
|
|
1039
|
-
|
|
1353
|
+
|
|
1354
|
+
daemon_stop_p = daemon_sub.add_parser("stop", help="Stop local daemon (sends SIGTERM)")
|
|
1355
|
+
|
|
1356
|
+
daemon_restart_p = daemon_sub.add_parser(
|
|
1357
|
+
"restart",
|
|
1358
|
+
help="Restart local daemon (stop + start in background)",
|
|
1359
|
+
)
|
|
1360
|
+
daemon_restart_p.add_argument("--server-url", default=None, help="Server URL to connect to")
|
|
1361
|
+
|
|
1362
|
+
daemon_status_p = daemon_sub.add_parser(
|
|
1363
|
+
"status",
|
|
1364
|
+
help="Show local daemon status and registered agents",
|
|
1365
|
+
)
|
|
1040
1366
|
daemon_status_p.add_argument("--all", action="store_true", help="List all runtimes (platform admin only)")
|
|
1041
|
-
|
|
1367
|
+
daemon_status_p.add_argument("-v", "--verbose", action="store_true", help="Expanded per-agent detail")
|
|
1368
|
+
|
|
1369
|
+
daemon_logs_p = daemon_sub.add_parser(
|
|
1370
|
+
"logs",
|
|
1371
|
+
help="Show daemon log output (optionally stream like tail -f)",
|
|
1372
|
+
)
|
|
1373
|
+
daemon_logs_p.add_argument(
|
|
1374
|
+
"-n", "--lines",
|
|
1375
|
+
type=int, default=50, metavar="N",
|
|
1376
|
+
help="Show last N lines before following (default: 50)",
|
|
1377
|
+
)
|
|
1378
|
+
daemon_logs_p.add_argument(
|
|
1379
|
+
"-f", "--follow",
|
|
1380
|
+
action="store_true",
|
|
1381
|
+
help="Stream new log lines as they arrive (like tail -f)",
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
daemon_agents_p = daemon_sub.add_parser(
|
|
1385
|
+
"agents",
|
|
1386
|
+
help="List all AI agents across registered daemons",
|
|
1387
|
+
)
|
|
1388
|
+
daemon_agents_p.add_argument("--all", action="store_true", help="Include all users' runtimes (admin only)")
|
|
1042
1389
|
|
|
1043
1390
|
# runtimes
|
|
1044
1391
|
rt_p = sub.add_parser("runtimes", help="Runtime management")
|
|
@@ -1135,9 +1482,12 @@ def main() -> None:
|
|
|
1135
1482
|
"set": cmd_config_set,
|
|
1136
1483
|
}.get(a.config_cmd, lambda _: config_p.print_help())(a),
|
|
1137
1484
|
"daemon": lambda a: {
|
|
1138
|
-
"start":
|
|
1139
|
-
"
|
|
1140
|
-
"
|
|
1485
|
+
"start": cmd_daemon_start,
|
|
1486
|
+
"stop": cmd_daemon_stop,
|
|
1487
|
+
"restart": cmd_daemon_restart,
|
|
1488
|
+
"status": cmd_daemon_status,
|
|
1489
|
+
"logs": cmd_daemon_logs,
|
|
1490
|
+
"agents": cmd_daemon_agents,
|
|
1141
1491
|
}.get(a.daemon_cmd, lambda _: daemon_p.print_help())(a),
|
|
1142
1492
|
"runtimes": lambda a: {
|
|
1143
1493
|
"list": cmd_runtimes_list,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|