forgexa-cli 1.45.5__tar.gz → 1.46.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/agent_core.py +11 -6
  4. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/daemon.py +49 -1
  5. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/pyproject.toml +1 -1
  7. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/README.md +0 -0
  8. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  14. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  15. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  16. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/requires.txt +0 -0
  17. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  18. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/setup.cfg +0 -0
  19. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_auth_and_runtime_commands.py +0 -0
  20. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_autoupgrade.py +0 -0
  21. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_check_command.py +0 -0
  22. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  23. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_local_bind_commands.py +0 -0
  24. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_runtime_credentials.py +0 -0
  25. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_session_credentials.py +0 -0
  26. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_silent_install.py +0 -0
  27. {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.45.5
3
+ Version: 1.46.0
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.45.5"
2
+ __version__ = "1.46.0"
@@ -505,12 +505,12 @@ class AgentDiscovery:
505
505
  cmd = custom_path or spec["commands"][0]
506
506
  resolved = shutil.which(cmd)
507
507
  if resolved:
508
- version = await self._get_version(spec["detect"])
508
+ version = await self._get_version(spec["detect"], resolved)
509
509
  if version is None and "detect_alt" in spec:
510
510
  # Primary detect command failed; try the alternative.
511
511
  # Example: new GitHub Copilot CLI 1.0.x uses `copilot version`
512
512
  # as a subcommand while older releases used `copilot --version`.
513
- version = await self._get_version(spec["detect_alt"])
513
+ version = await self._get_version(spec["detect_alt"], resolved)
514
514
  if version is None:
515
515
  # Binary found but version check failed — it is a stub or
516
516
  # not properly installed (e.g. copilot prompts to install).
@@ -540,7 +540,9 @@ class AgentDiscovery:
540
540
  logger.info("Discovered agent: %s v%s (%s)", agent_id, version, resolved)
541
541
  return available
542
542
 
543
- async def _get_version(self, detect_cmd: str) -> str | None:
543
+ async def _get_version(
544
+ self, detect_cmd: str, executable: str | None = None,
545
+ ) -> str | None:
544
546
  """Run <detect_cmd> and return the first line of output as a version string.
545
547
 
546
548
  Returns ``None`` if the command exits with a non-zero code, times out,
@@ -551,9 +553,12 @@ class AgentDiscovery:
551
553
  import re
552
554
  try:
553
555
  parts = detect_cmd.split()
554
- resolved = shutil.which(parts[0])
555
- if resolved:
556
- parts[0] = resolved
556
+ if executable:
557
+ parts[0] = executable
558
+ else:
559
+ resolved = shutil.which(parts[0])
560
+ if resolved:
561
+ parts[0] = resolved
557
562
  parts = _prepare_agent_command(parts, preserve_argv=False)
558
563
  proc = await asyncio.create_subprocess_exec(
559
564
  *parts,
@@ -1067,7 +1067,7 @@ except (ImportError, ModuleNotFoundError):
1067
1067
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1068
1068
  # Kept in sync with pyproject.toml version via bump-version.sh.
1069
1069
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1070
- DAEMON_VERSION = "1.45.5"
1070
+ DAEMON_VERSION = "1.46.0"
1071
1071
 
1072
1072
 
1073
1073
  def _detect_client_type() -> str:
@@ -8593,6 +8593,8 @@ class RuntimeDaemon:
8593
8593
  Task execution is shared across all servers with a single concurrent task limit.
8594
8594
  """
8595
8595
 
8596
+ AGENT_REFRESH_INTERVAL_SECONDS = 60
8597
+
8596
8598
  def __init__(self):
8597
8599
  # Compute stable hardware fingerprint from machine-id / MAC address.
8598
8600
  # This allows the server to identify the same physical machine even when
@@ -8627,6 +8629,7 @@ class RuntimeDaemon:
8627
8629
 
8628
8630
  self.connections: list[ServerConnection] = []
8629
8631
  self.agent_discovery = AgentDiscovery()
8632
+ self._agent_refresh_task: asyncio.Task | None = None
8630
8633
  self.workspace_manager = WorkspaceManager(self.workspaces_root)
8631
8634
  self.process_manager = ProcessManager()
8632
8635
  self._lock_file = None # File lock to prevent multiple daemon instances
@@ -8657,6 +8660,41 @@ class RuntimeDaemon:
8657
8660
  self._project_execution_locks[project_key] = asyncio.Lock()
8658
8661
  return self._project_execution_locks[project_key]
8659
8662
 
8663
+ async def _refresh_agents(self) -> None:
8664
+ """Re-discover local CLIs so upgrades reach the runtime heartbeat."""
8665
+ try:
8666
+ refreshed_agents = await self.agent_discovery.discover()
8667
+ except Exception as exc:
8668
+ logger.warning("Agent inventory refresh failed: %s", exc)
8669
+ return
8670
+
8671
+ if refreshed_agents == self.agents:
8672
+ return
8673
+
8674
+ previous_agents = ", ".join(
8675
+ f"{agent.agent_id} v{agent.version}" for agent in self.agents
8676
+ ) or "none"
8677
+ current_agents = ", ".join(
8678
+ f"{agent.agent_id} v{agent.version}" for agent in refreshed_agents
8679
+ ) or "none"
8680
+ self.agents = refreshed_agents
8681
+ logger.info("Refreshed agent inventory: %s -> %s", previous_agents, current_agents)
8682
+
8683
+ active_tasks = len(self.active_tasks)
8684
+ agent_dicts = self._agents_as_dicts()
8685
+ for connection in self.connections:
8686
+ if connection.heartbeat:
8687
+ connection.heartbeat.update(active_tasks, agent_dicts)
8688
+
8689
+ async def _agent_refresh_loop(self) -> None:
8690
+ while not self._shutdown:
8691
+ try:
8692
+ await asyncio.sleep(self.AGENT_REFRESH_INTERVAL_SECONDS)
8693
+ except asyncio.CancelledError:
8694
+ return
8695
+ if not self._shutdown:
8696
+ await self._refresh_agents()
8697
+
8660
8698
  def _publish_local_runtime_registration(self, server_url: str, runtime_id: str) -> None:
8661
8699
  issuer = _normalize_issuer_url(server_url)
8662
8700
  if not issuer or not runtime_id:
@@ -9130,6 +9168,8 @@ class RuntimeDaemon:
9130
9168
  logger.info("Daemon ready. Connected to %d server(s). Polling for tasks...",
9131
9169
  len(self.connections))
9132
9170
 
9171
+ self._agent_refresh_task = asyncio.create_task(self._agent_refresh_loop())
9172
+
9133
9173
  # Clean up stale Copilot/Claude temp directories left by prior OOM-killed
9134
9174
  # daemon processes. This runs once at startup so /tmp doesn't fill up.
9135
9175
  await self._cleanup_stale_tmp_dirs()
@@ -14581,6 +14621,14 @@ class RuntimeDaemon:
14581
14621
  logger.info("Shutting down daemon...")
14582
14622
  self._shutdown = True
14583
14623
 
14624
+ if self._agent_refresh_task:
14625
+ self._agent_refresh_task.cancel()
14626
+ try:
14627
+ await self._agent_refresh_task
14628
+ except asyncio.CancelledError:
14629
+ pass
14630
+ self._agent_refresh_task = None
14631
+
14584
14632
  # Cancel task coroutines, then terminate and reap every tracked process.
14585
14633
  tasks = list(self.active_tasks.values())
14586
14634
  for task in tasks:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.45.5
3
+ Version: 1.46.0
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.45.5"
3
+ version = "1.46.0"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
File without changes
File without changes