forgexa-cli 1.48.3__tar.gz → 1.49.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 (28) hide show
  1. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/agent_core.py +96 -0
  4. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/daemon.py +111 -24
  5. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/pyproject.toml +1 -1
  7. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/README.md +0 -0
  8. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  15. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  16. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  17. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/requires.txt +0 -0
  18. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  19. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/setup.cfg +0 -0
  20. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_auth_and_runtime_commands.py +0 -0
  21. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_autoupgrade.py +0 -0
  22. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_check_command.py +0 -0
  23. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  24. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_local_bind_commands.py +0 -0
  25. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_runtime_credentials.py +0 -0
  26. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_session_credentials.py +0 -0
  27. {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_silent_install.py +0 -0
  28. {forgexa_cli-1.48.3 → forgexa_cli-1.49.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.48.3
3
+ Version: 1.49.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.48.3"
2
+ __version__ = "1.49.0"
@@ -319,6 +319,21 @@ class AgentDiscovery:
319
319
  ),
320
320
  "upgrade_hint": "opencode upgrade",
321
321
  },
322
+ "mimo": {
323
+ "commands": ["mimo"],
324
+ "detect": "mimo --version",
325
+ "invoke_modes": ["cli"],
326
+ "env_path_override": "FACTORY_MIMO_PATH",
327
+ "env_model_override": "FACTORY_MIMO_MODEL",
328
+ "compatibility_level": "L2",
329
+ "min_version": (0, 1, 14),
330
+ "min_version_reason": (
331
+ "The Runtime Daemon relies on the verified `mimo run --format json` "
332
+ "protocol and non-interactive permission flag introduced by MiMo Code "
333
+ "v0.1.14."
334
+ ),
335
+ "upgrade_hint": "mimo upgrade",
336
+ },
322
337
  "gemini": {
323
338
  "commands": ["gemini"],
324
339
  "detect": "gemini --version",
@@ -380,6 +395,7 @@ class AgentDiscovery:
380
395
  extra_dirs += [
381
396
  appdata / "npm", # npm -g installs
382
397
  home / ".opencode" / "bin",
398
+ home / ".mimocode" / "bin",
383
399
  home / ".cargo" / "bin",
384
400
  home / ".bun" / "bin",
385
401
  home / "scoop" / "shims", # scoop package manager
@@ -422,6 +438,7 @@ class AgentDiscovery:
422
438
  extra_dirs += [
423
439
  home / ".local" / "bin", # pip / pipx installs, Claude Code
424
440
  home / ".opencode" / "bin", # opencode
441
+ home / ".mimocode" / "bin", # MiMo Code
425
442
  home / ".cargo" / "bin", # Rust / cargo installs
426
443
  home / ".bun" / "bin", # bun
427
444
  home / ".volta" / "bin", # volta (node version manager)
@@ -658,6 +675,14 @@ class AgentDiscovery:
658
675
  # ── Advisory file locks ──
659
676
 
660
677
 
678
+ def mimo_profile_lock_path() -> Path:
679
+ """Return the machine-local lock shared by all MiMo Code processes."""
680
+ override = os.environ.get("FORGEXA_MIMO_PROFILE_LOCK", "").strip()
681
+ if override:
682
+ return Path(override)
683
+ return Path.home() / ".forgexa" / "locks" / "mimo-profile.lock"
684
+
685
+
661
686
  def acquire_file_lock(
662
687
  path: str | Path,
663
688
  *,
@@ -1616,6 +1641,77 @@ def build_opencode_command(
1616
1641
  return cmd
1617
1642
 
1618
1643
 
1644
+ def build_mimo_command(
1645
+ binary: str,
1646
+ *,
1647
+ cwd: str | Path,
1648
+ model: str | None = None,
1649
+ prompt_file: str | Path | None = None,
1650
+ session_id: str | None = None,
1651
+ prompt: str | None = None,
1652
+ ) -> list[str]:
1653
+ """MiMo Code CLI non-interactive invocation for the local AgentHost."""
1654
+ cmd = [
1655
+ binary, "run",
1656
+ "--format", "json",
1657
+ "--dangerously-skip-permissions",
1658
+ "--dir", str(cwd),
1659
+ ]
1660
+ if session_id:
1661
+ cmd += ["--session", session_id]
1662
+ if model:
1663
+ cmd += ["--model", model]
1664
+ if prompt_file is not None:
1665
+ cmd += [
1666
+ "--file", str(prompt_file),
1667
+ "--", "Read and follow the attached instructions.",
1668
+ ]
1669
+ elif prompt is not None:
1670
+ cmd += ["--", prompt]
1671
+ return cmd
1672
+
1673
+
1674
+ def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
1675
+ """Return the constrained MiMo Code environment used by AgentHost turns."""
1676
+ env = dict(env)
1677
+ env.update({
1678
+ "MIMOCODE_DISABLE_SHARE": "true",
1679
+ "MIMOCODE_ENABLE_ANALYSIS": "false",
1680
+ "MIMOCODE_PURE": "true",
1681
+ "MIMOCODE_DISABLE_DEFAULT_PLUGINS": "true",
1682
+ "MIMOCODE_DISABLE_PROJECT_CONFIG": "true",
1683
+ "MIMOCODE_DISABLE_EXTERNAL_SKILLS": "true",
1684
+ "MIMOCODE_DISABLE_LSP_DOWNLOAD": "true",
1685
+ "MIMOCODE_DISABLE_CLAUDE_CODE_MCP": "true",
1686
+ "MIMOCODE_DISABLE_AUTOUPDATE": "true",
1687
+ "MIMOCODE_MIMO_ONLY": "true",
1688
+ "MIMOCODE_PERMISSION": json.dumps({
1689
+ "*": "deny",
1690
+ "read": "allow",
1691
+ "edit": "allow",
1692
+ "glob": "allow",
1693
+ "grep": "allow",
1694
+ "lsp": "allow",
1695
+ "bash": {
1696
+ "*": "allow",
1697
+ "git commit*": "deny",
1698
+ "git * commit*": "deny",
1699
+ "git push*": "deny",
1700
+ "git * push*": "deny",
1701
+ "git reset --hard*": "deny",
1702
+ "git * reset --hard*": "deny",
1703
+ },
1704
+ "external_directory": "deny",
1705
+ "task": "deny",
1706
+ "skill": "deny",
1707
+ "webfetch": "deny",
1708
+ "websearch": "deny",
1709
+ "codesearch": "deny",
1710
+ }, separators=(",", ":")),
1711
+ })
1712
+ return env
1713
+
1714
+
1619
1715
  def opencode_base_env(
1620
1716
  env: dict[str, str], *, data_home: str | Path, require_windows_shell: bool = False,
1621
1717
  ) -> dict[str, str]:
@@ -1120,7 +1120,7 @@ except (ImportError, ModuleNotFoundError):
1120
1120
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1121
1121
  # Kept in sync with pyproject.toml version via bump-version.sh.
1122
1122
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1123
- DAEMON_VERSION = "1.48.3"
1123
+ DAEMON_VERSION = "1.49.0"
1124
1124
 
1125
1125
 
1126
1126
  def _detect_client_type() -> str:
@@ -1934,7 +1934,7 @@ def _daemon_extract_agent_output(agent_id: str, raw: str) -> str:
1934
1934
  return _daemon_extract_copilot_output(raw)
1935
1935
  if agent_id == "codex":
1936
1936
  return _daemon_extract_codex_output(raw)
1937
- if agent_id == "opencode":
1937
+ if agent_id in ("opencode", "mimo"):
1938
1938
  return _daemon_extract_opencode_output(raw)
1939
1939
  return raw
1940
1940
 
@@ -5640,6 +5640,10 @@ class ProcessManager:
5640
5640
  result = await self._run_codex(agent, prompt, workspace_path, timeout, task.task_id, on_chunk)
5641
5641
  elif agent.agent_id == "opencode":
5642
5642
  result = await self._run_opencode(agent, prompt, workspace_path, timeout, task.task_id, on_chunk)
5643
+ elif agent.agent_id == "mimo":
5644
+ result = await self._run_mimo(
5645
+ agent, prompt, workspace_path, timeout, task.task_id, on_chunk,
5646
+ )
5643
5647
  elif agent.agent_id == "gemini":
5644
5648
  result = await self._run_gemini(agent, prompt, workspace_path, timeout, task.task_id, on_chunk)
5645
5649
  elif agent.agent_id == "kimi":
@@ -6379,6 +6383,53 @@ class ProcessManager:
6379
6383
  result.metrics.update(parsed_metrics)
6380
6384
  return result
6381
6385
 
6386
+ async def _run_mimo(
6387
+ self, agent: DiscoveredAgent, prompt: str, cwd: Path, timeout: int, task_id: str,
6388
+ on_chunk: Any = None,
6389
+ ) -> TaskResult:
6390
+ """Run MiMo Code with its user-managed profile serialized machine-wide.
6391
+
6392
+ MiMo's native sessions, memory and credentials belong to its own profile.
6393
+ Unlike OpenCode, do not redirect XDG/MIMOCODE_HOME or copy auth files per
6394
+ task: that would break its state model and risk credential handling bugs.
6395
+ The shared lock covers AgentHost and Daemon processes using that profile.
6396
+ """
6397
+ profile_lock = agent_core.acquire_file_lock(agent_core.mimo_profile_lock_path())
6398
+ if profile_lock is None:
6399
+ return TaskResult(
6400
+ status="failed",
6401
+ exit_code=-1,
6402
+ stdout="",
6403
+ stderr="",
6404
+ error=(
6405
+ "MiMo Code profile is busy with another local turn; retry after it "
6406
+ "finishes"
6407
+ ),
6408
+ failure_code="mimo_profile_busy",
6409
+ )
6410
+
6411
+ tmp_root = tempfile.mkdtemp(prefix=f"mimo-{task_id[:8]}-")
6412
+ try:
6413
+ prompt_file = Path(tmp_root) / "instructions.md"
6414
+ prompt_file.write_text(prompt, encoding="utf-8")
6415
+ cmd = agent_core.build_mimo_command(
6416
+ agent.command,
6417
+ cwd=cwd,
6418
+ model=os.environ.get("FACTORY_MIMO_MODEL", "").strip() or None,
6419
+ prompt_file=prompt_file,
6420
+ )
6421
+ env = agent_core.mimo_base_env(os.environ.copy())
6422
+ result = await self._run_cli(
6423
+ cmd, cwd, timeout, task_id, on_chunk=on_chunk, env=env,
6424
+ )
6425
+ finally:
6426
+ agent_core.release_file_lock(profile_lock)
6427
+ shutil.rmtree(tmp_root, ignore_errors=True)
6428
+
6429
+ parsed_metrics = self._parse_agent_jsonl_output(result.stdout)
6430
+ result.metrics.update(parsed_metrics)
6431
+ return result
6432
+
6382
6433
  async def _run_gemini(
6383
6434
  self, agent: DiscoveredAgent, prompt: str, cwd: Path, timeout: int, task_id: str,
6384
6435
  on_chunk: Any = None,
@@ -7830,7 +7881,7 @@ class HeartbeatService:
7830
7881
 
7831
7882
  def update(self, active_tasks: int, agents: list[dict]):
7832
7883
  self._active_tasks = active_tasks
7833
- self._agents = agents
7884
+ self._agents = [dict(agent) for agent in agents]
7834
7885
 
7835
7886
  async def _sync_local_workspace_bindings(self) -> None:
7836
7887
  try:
@@ -8143,6 +8194,8 @@ class ServerConnection:
8143
8194
  self.poller: TaskPoller | None = None
8144
8195
  self.reporter: ProgressReporter | None = None
8145
8196
  self.log_uploader: LogUploader | None = None
8197
+ self._agents: list[DiscoveredAgent] = []
8198
+ self._max_concurrent = 5
8146
8199
  self._auth_failures = 0 # Consecutive auth failure count
8147
8200
  self._max_auth_failures = 3 # Trigger re-registration after this many
8148
8201
  # Set when the server demands a runtime credential (compat window
@@ -8568,7 +8621,19 @@ class ServerConnection:
8568
8621
  self._re_register_lock = asyncio.Lock()
8569
8622
  return self._re_register_lock
8570
8623
 
8571
- async def re_register(self, agents: list[DiscoveredAgent], max_concurrent: int):
8624
+ def update_agent_inventory(
8625
+ self, active_tasks: int, agents: list[DiscoveredAgent],
8626
+ ) -> None:
8627
+ """Keep heartbeat and re-registration on one current agent snapshot."""
8628
+ self._agents = list(agents)
8629
+ if self.heartbeat:
8630
+ self.heartbeat.update(active_tasks, _agent_dicts_for_wire(self._agents))
8631
+
8632
+ async def re_register(
8633
+ self,
8634
+ agents: list[DiscoveredAgent] | None = None,
8635
+ max_concurrent: int | None = None,
8636
+ ):
8572
8637
  """Refresh token and re-register with the server.
8573
8638
 
8574
8639
  Called when persistent auth failures indicate stale token/hash.
@@ -8578,6 +8643,10 @@ class ServerConnection:
8578
8643
  Single-flight: concurrent triggers (heartbeat/poller/poll_ai_jobs all
8579
8644
  share the auth-failure callback) collapse into one execution.
8580
8645
  """
8646
+ if agents is not None:
8647
+ self._agents = list(agents)
8648
+ if max_concurrent is not None:
8649
+ self._max_concurrent = max_concurrent
8581
8650
  lock = self._get_re_register_lock()
8582
8651
  if lock.locked():
8583
8652
  logger.debug(
@@ -8588,7 +8657,7 @@ class ServerConnection:
8588
8657
  async with lock:
8589
8658
  await self.refresh_access_token()
8590
8659
  try:
8591
- await self.register(agents, max_concurrent)
8660
+ await self.register(self._agents, self._max_concurrent)
8592
8661
  # Sync runtime_id to all services (may change after re-registration)
8593
8662
  if self.heartbeat and self.runtime_id:
8594
8663
  self.heartbeat.runtime_id = self.runtime_id
@@ -8610,9 +8679,10 @@ class ServerConnection:
8610
8679
 
8611
8680
  async def register(self, agents: list[DiscoveredAgent], max_concurrent: int):
8612
8681
  """Register this daemon with the server."""
8613
- self._agents = agents
8682
+ self._agents = list(agents)
8683
+ self._max_concurrent = max_concurrent
8614
8684
  self._max_concurrent_cache = max_concurrent
8615
- agent_dicts = _agent_dicts_for_wire(agents)
8685
+ agent_dicts = _agent_dicts_for_wire(self._agents)
8616
8686
  async def _register_once():
8617
8687
  payload = {
8618
8688
  "daemon_id": self.daemon_id,
@@ -8685,7 +8755,7 @@ class ServerConnection:
8685
8755
  preflight_snapshot_provider: Callable[[], dict | None] | None = None,
8686
8756
  ):
8687
8757
  """Initialize heartbeat, poller, and reporter services."""
8688
- self._agents = agents
8758
+ self._agents = list(agents)
8689
8759
  self._max_concurrent = getattr(self, '_max_concurrent_cache', 5)
8690
8760
 
8691
8761
  def _on_auth_success():
@@ -8708,7 +8778,7 @@ class ServerConnection:
8708
8778
  try:
8709
8779
  loop = asyncio.get_event_loop()
8710
8780
  if loop.is_running():
8711
- loop.create_task(self.re_register(self._agents, self._max_concurrent))
8781
+ loop.create_task(self.re_register())
8712
8782
  except Exception:
8713
8783
  pass
8714
8784
 
@@ -8719,7 +8789,7 @@ class ServerConnection:
8719
8789
  )
8720
8790
  self.heartbeat._on_success = _on_heartbeat_success
8721
8791
  self.heartbeat._on_auth_failure = _on_auth_failure
8722
- self.heartbeat.update(0, _agent_dicts_for_wire(agents))
8792
+ self.update_agent_inventory(0, self._agents)
8723
8793
  self.poller = TaskPoller(
8724
8794
  self.client, self.server_url, self.runtime_id, poll_interval,
8725
8795
  )
@@ -9354,10 +9424,8 @@ class RuntimeDaemon:
9354
9424
  logger.info("Refreshed agent inventory: %s -> %s", previous_agents, current_agents)
9355
9425
 
9356
9426
  active_tasks = len(self.active_tasks)
9357
- agent_dicts = self._agents_as_dicts()
9358
9427
  for connection in self.connections:
9359
- if connection.heartbeat:
9360
- connection.heartbeat.update(active_tasks, agent_dicts)
9428
+ connection.update_agent_inventory(active_tasks, refreshed_agents)
9361
9429
 
9362
9430
  async def _agent_refresh_loop(self) -> None:
9363
9431
  while not self._shutdown:
@@ -9493,6 +9561,11 @@ class RuntimeDaemon:
9493
9561
  for field, value in attachment.items():
9494
9562
  if value and not existing.get(field):
9495
9563
  existing[field] = value
9564
+ if download_url and download_url != existing.get("download_url"):
9565
+ aliases = list(existing.get("_download_url_aliases") or [])
9566
+ if download_url not in aliases:
9567
+ aliases.append(download_url)
9568
+ existing["_download_url_aliases"] = aliases
9496
9569
  existing_artifact_id = str(
9497
9570
  existing.get("artifact_id") or existing.get("id") or ""
9498
9571
  ).strip()
@@ -9537,6 +9610,12 @@ class RuntimeDaemon:
9537
9610
  for field, value in attachment.items():
9538
9611
  if value and not identified.get(field):
9539
9612
  identified[field] = value
9613
+ legacy_url = attachment.get("download_url")
9614
+ if isinstance(legacy_url, str) and legacy_url and legacy_url != identified.get("download_url"):
9615
+ aliases = list(identified.get("_download_url_aliases") or [])
9616
+ if legacy_url not in aliases:
9617
+ aliases.append(legacy_url)
9618
+ identified["_download_url_aliases"] = aliases
9540
9619
  return deduplicated
9541
9620
 
9542
9621
  @staticmethod
@@ -9551,14 +9630,14 @@ class RuntimeDaemon:
9551
9630
  @staticmethod
9552
9631
  def _rewrite_staged_attachment_references(
9553
9632
  prompt: str,
9554
- staged: list[tuple[str, str, str]],
9633
+ staged: list[tuple[str, list[str], str]],
9555
9634
  ) -> str:
9556
9635
  if not staged:
9557
9636
  return prompt
9558
9637
 
9559
9638
  local_files = []
9560
- for file_name, download_url, local_path in staged:
9561
- if download_url:
9639
+ for file_name, download_urls, local_path in staged:
9640
+ for download_url in download_urls:
9562
9641
  command_pattern = r"`curl -L '" + re.escape(download_url) + r"' -o /tmp/[^`]+`"
9563
9642
  prompt = re.sub(command_pattern, lambda _match, path=local_path: f"`{path}`", prompt)
9564
9643
  prompt = prompt.replace(f"Download: {download_url}", f"Staged local file: `{local_path}`")
@@ -9685,7 +9764,7 @@ class RuntimeDaemon:
9685
9764
  )
9686
9765
 
9687
9766
  staging_dir = Path(tempfile.mkdtemp(prefix=f"forgexa-attachments-{task.task_id[:8]}-"))
9688
- staged: list[tuple[str, str, str]] = []
9767
+ staged: list[tuple[str, list[str], str]] = []
9689
9768
  deadline = time.monotonic() + self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS
9690
9769
  remaining_bytes = self._TASK_ATTACHMENT_MAX_TOTAL_BYTES
9691
9770
  try:
@@ -9694,6 +9773,10 @@ class RuntimeDaemon:
9694
9773
  attachment.get("file_path") or attachment.get("file_name") or "attachment"
9695
9774
  )
9696
9775
  download_url = str(attachment.get("download_url") or "")
9776
+ download_url_aliases = [download_url] if download_url else []
9777
+ for alias in attachment.get("_download_url_aliases") or []:
9778
+ if isinstance(alias, str) and alias and alias not in download_url_aliases:
9779
+ download_url_aliases.append(alias)
9697
9780
  destination = staging_dir / self._task_attachment_filename(attachment, position)
9698
9781
  try:
9699
9782
  expected_size = int(attachment.get("size_bytes") or 0)
@@ -9735,7 +9818,7 @@ class RuntimeDaemon:
9735
9818
  timeout_seconds=remaining_seconds,
9736
9819
  )
9737
9820
  remaining_bytes -= destination.stat().st_size
9738
- staged.append((file_name, download_url, str(destination)))
9821
+ staged.append((file_name, download_url_aliases, str(destination)))
9739
9822
  except Exception:
9740
9823
  shutil.rmtree(staging_dir, ignore_errors=True)
9741
9824
  raise
@@ -10223,8 +10306,7 @@ class RuntimeDaemon:
10223
10306
  # Update heartbeat on all connections with total active count
10224
10307
  total_active = len(self.active_tasks)
10225
10308
  for conn in self.connections:
10226
- if conn.heartbeat:
10227
- conn.heartbeat.update(total_active, self._agents_as_dicts())
10309
+ conn.update_agent_inventory(total_active, self.agents)
10228
10310
 
10229
10311
  # Poll from all connected servers
10230
10312
  for conn in self.connections:
@@ -10622,6 +10704,7 @@ class RuntimeDaemon:
10622
10704
  "claude": "npm install -g @anthropic-ai/claude-code",
10623
10705
  "codex": "npm install -g @openai/codex",
10624
10706
  "opencode": "curl -sSL https://opencode.ai/install | sh",
10707
+ "mimo": "curl -fsSL https://mimo.xiaomi.com/install | bash",
10625
10708
  "gemini": "npm install -g @google/gemini-cli",
10626
10709
  "kimi": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
10627
10710
  "copilot": (
@@ -11668,9 +11751,11 @@ class RuntimeDaemon:
11668
11751
  for agent in self.agents:
11669
11752
  if agent.agent_id == preferred_id:
11670
11753
  return agent
11671
- # 3. Any remaining agent
11754
+ # 3. Any remaining agent. MiMo Code requires explicit selection or an
11755
+ # explicit policy fallback entry; it must never become an implicit
11756
+ # catch-all because its profile state is shared machine-wide.
11672
11757
  for agent in self.agents:
11673
- if agent.agent_id not in tried:
11758
+ if agent.agent_id not in tried and agent.agent_id != "mimo":
11674
11759
  return agent
11675
11760
 
11676
11761
  return None
@@ -11713,8 +11798,9 @@ class RuntimeDaemon:
11713
11798
  for agent in self.agents:
11714
11799
  if agent.compatibility_level == "L3":
11715
11800
  return agent
11716
- # 5. Any agent (last resort for unrecognized types)
11717
- return self.agents[0] if self.agents else None
11801
+ # 5. Any agent (last resort for unrecognized types), excluding the
11802
+ # explicit-only MiMo Code driver.
11803
+ return next((agent for agent in self.agents if agent.agent_id != "mimo"), None)
11718
11804
 
11719
11805
  # ── Layer 2: Validation Gate ──
11720
11806
 
@@ -13028,6 +13114,7 @@ class RuntimeDaemon:
13028
13114
  "claude": "npm install -g @anthropic-ai/claude-code",
13029
13115
  "codex": "npm install -g @openai/codex",
13030
13116
  "opencode": "curl -sSL https://opencode.ai/install | sh",
13117
+ "mimo": "curl -fsSL https://mimo.xiaomi.com/install | bash",
13031
13118
  "gemini": "npm install -g @google/gemini-cli",
13032
13119
  "kimi": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
13033
13120
  "copilot": (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.48.3
3
+ Version: 1.49.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.48.3"
3
+ version = "1.49.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