forgexa-cli 1.26.0__tar.gz → 1.26.2__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.26.0 → forgexa_cli-1.26.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/daemon.py +12 -4
  4. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/main.py +14 -0
  5. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/pyproject.toml +1 -1
  7. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_local_bind_commands.py +58 -0
  8. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/README.md +0 -0
  9. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/agent_core.py +0 -0
  12. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/autoupgrade.py +0 -0
  13. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli/py.typed +0 -0
  14. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  15. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  16. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  17. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli.egg-info/requires.txt +0 -0
  18. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  19. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/setup.cfg +0 -0
  20. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_auth_and_runtime_commands.py +0 -0
  21. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_autoupgrade.py +0 -0
  22. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_check_command.py +0 -0
  23. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  24. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_runtime_credentials.py +0 -0
  25. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_session_credentials.py +0 -0
  26. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/tests/test_silent_install.py +0 -0
  27. {forgexa_cli-1.26.0 → forgexa_cli-1.26.2}/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.26.0
3
+ Version: 1.26.2
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.26.0"
2
+ __version__ = "1.26.2"
@@ -909,7 +909,7 @@ except (ImportError, ModuleNotFoundError):
909
909
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
910
910
  # Kept in sync with pyproject.toml version via bump-version.sh.
911
911
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
912
- DAEMON_VERSION = "1.26.0"
912
+ DAEMON_VERSION = "1.26.2"
913
913
 
914
914
 
915
915
  def _detect_client_type() -> str:
@@ -971,6 +971,14 @@ logger = logging.getLogger("daemon")
971
971
  logger.info("Daemon log: %s", DAEMON_LOG_PATH)
972
972
 
973
973
 
974
+ def _format_exc(exc: BaseException) -> str:
975
+ """Render an exception for logging, always including its type name so a
976
+ str()-less exception (e.g. an empty httpx/asyncio timeout) never logs as
977
+ a blank, undiagnosable "... error: " line."""
978
+ detail = str(exc)
979
+ return f"{type(exc).__name__}: {detail}" if detail else type(exc).__name__
980
+
981
+
974
982
  # ── Hardware ID — stable cross-IP machine fingerprint ──
975
983
 
976
984
 
@@ -6776,7 +6784,7 @@ class HeartbeatService:
6776
6784
  else:
6777
6785
  logger.warning("Heartbeat failed: %s %s", resp.status_code, resp.text)
6778
6786
  except Exception as e:
6779
- logger.warning("Heartbeat error: %s", e)
6787
+ logger.warning("Heartbeat error: %s", _format_exc(e))
6780
6788
 
6781
6789
  await asyncio.sleep(self.interval)
6782
6790
 
@@ -6940,7 +6948,7 @@ class TaskPoller:
6940
6948
  ))
6941
6949
  return tasks
6942
6950
  except Exception as e:
6943
- logger.warning("Task poll error: %s", e)
6951
+ logger.warning("Task poll error: %s", _format_exc(e))
6944
6952
  return []
6945
6953
 
6946
6954
  async def poll_ai_jobs(self) -> list[dict]:
@@ -6964,7 +6972,7 @@ class TaskPoller:
6964
6972
  self.cancelled_ai_job_ids = []
6965
6973
  return []
6966
6974
  except Exception as e:
6967
- logger.debug("AIJob poll error: %s", e)
6975
+ logger.debug("AIJob poll error: %s", _format_exc(e))
6968
6976
  self.cancelled_ai_job_ids = []
6969
6977
  return []
6970
6978
 
@@ -2859,6 +2859,20 @@ def cmd_project_bind_local(args: argparse.Namespace) -> None:
2859
2859
  sys.exit(1)
2860
2860
 
2861
2861
  mapping = load_local_workspaces_mapping()
2862
+ local_path_str = str(local_path)
2863
+ # A folder can only ever be validly bound to one binding_id at a time -
2864
+ # drop any other (necessarily stale) entry already pointing at this same
2865
+ # path first. Without this, a stale entry left behind by an earlier
2866
+ # attempt (e.g. a re-registered runtime after a reinstall, or a
2867
+ # verification that timed out client-side) can linger alongside the new
2868
+ # one, and agent_host.py's `_match_local_binding_id` (a plain path scan
2869
+ # with no tie-breaking) can resolve to the wrong, orphaned binding_id
2870
+ # instead of the one just verified.
2871
+ mapping = {
2872
+ key: entry
2873
+ for key, entry in mapping.items()
2874
+ if key == binding_id or entry.get("local_path") != local_path_str
2875
+ }
2862
2876
  mapping[binding_id] = {
2863
2877
  "project_id": args.project,
2864
2878
  "user_id": user_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.26.0
3
+ Version: 1.26.2
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.26.0"
3
+ version = "1.26.2"
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"
@@ -70,6 +70,64 @@ def test_bind_local_happy_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
70
70
  }
71
71
 
72
72
 
73
+ def test_bind_local_replaces_stale_entry_for_same_local_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
74
+ """A stale entry from an earlier attempt (e.g. before a daemon/runtime
75
+ reinstall) must not linger alongside a freshly-verified one for the same
76
+ folder - agent_host.py's _match_local_binding_id does a plain path scan
77
+ with no tie-breaking, so a leftover duplicate can resolve to the wrong,
78
+ orphaned binding_id and fail turn.start with binding_verification_required
79
+ even though the IDE just linked successfully."""
80
+ monkeypatch.setenv("HOME", str(tmp_path))
81
+ repo = tmp_path / "my-real-project"
82
+ _init_git_repo_with_remote(repo)
83
+
84
+ mapping_path = tmp_path / ".forgexa" / "local-workspaces.json"
85
+ mapping_path.parent.mkdir(parents=True, exist_ok=True)
86
+ mapping_path.write_text(json.dumps({
87
+ "binding-old": {
88
+ "project_id": "project-1",
89
+ "user_id": "user-1",
90
+ "runtime_id": "runtime-old",
91
+ "repo_identity": "github.com/acme/widgets",
92
+ "local_path": str(repo.resolve()),
93
+ },
94
+ }))
95
+
96
+ monkeypatch.setattr(main, "_get_runtimes", lambda: [
97
+ {"id": "runtime-new", "pool_type": "personal", "daemon_id": "d2", "device_name": "laptop"},
98
+ ])
99
+
100
+ def fake_get(path: str):
101
+ if path == "/auth/me":
102
+ return {"id": "user-1"}
103
+ assert path == "/projects/project-1/local-workspace-bindings/binding-new"
104
+ return {"id": "binding-new", "status": "active"}
105
+
106
+ def fake_post(path: str, data=None):
107
+ if path.endswith("/local-workspace-bindings"):
108
+ return {"id": "binding-new", "status": "pending"}
109
+ if path.endswith("/verify"):
110
+ return {"id": "binding-new", "status": "active"}
111
+ raise AssertionError(f"unexpected POST {path}")
112
+
113
+ monkeypatch.setattr(main, "_get", fake_get)
114
+ monkeypatch.setattr(main, "_post", fake_post)
115
+
116
+ args = SimpleNamespace(project="project-1", path=str(repo), runtime=None)
117
+ main.cmd_project_bind_local(args)
118
+
119
+ mapping = json.loads(mapping_path.read_text())
120
+ assert mapping == {
121
+ "binding-new": {
122
+ "project_id": "project-1",
123
+ "user_id": "user-1",
124
+ "runtime_id": "runtime-new",
125
+ "repo_identity": "github.com/acme/widgets",
126
+ "local_path": str(repo.resolve()),
127
+ },
128
+ }
129
+
130
+
73
131
  def test_bind_local_rejects_non_git_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
74
132
  monkeypatch.setenv("HOME", str(tmp_path))
75
133
  plain_dir = tmp_path / "not-a-repo"
File without changes
File without changes