forgexa-cli 1.62.4__tar.gz → 1.62.5__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 (29) hide show
  1. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/PKG-INFO +1 -1
  2. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/agent_core.py +138 -29
  4. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/daemon.py +1 -1
  5. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/pyproject.toml +1 -1
  7. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/README.md +0 -0
  8. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/setup.cfg +0 -0
  21. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.62.4 → forgexa_cli-1.62.5}/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.62.4
3
+ Version: 1.62.5
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.62.4"
2
+ __version__ = "1.62.5"
@@ -48,6 +48,7 @@ import logging
48
48
  import os
49
49
  import platform
50
50
  import re
51
+ import shlex
51
52
  import shutil
52
53
  import subprocess
53
54
  import tempfile
@@ -862,8 +863,11 @@ class AgentDiscovery:
862
863
  """Reject a Windows-side agent before its version probe can run via WSL interop."""
863
864
  if sys.platform != "linux" or not os.environ.get("WSL_DISTRO_NAME"):
864
865
  return False
865
- resolved = Path(path).resolve()
866
- if resolved.suffix.lower() in {".exe", ".cmd", ".bat"}:
866
+ try:
867
+ resolved = Path(path).resolve()
868
+ except (OSError, RuntimeError):
869
+ return True
870
+ if resolved.suffix.lower() in {".exe", ".cmd", ".bat", ".ps1", ".com"}:
867
871
  return True
868
872
  parts = resolved.parts
869
873
  if len(parts) >= 3 and parts[1] == "mnt" and len(parts[2]) == 1 and parts[2].isalpha():
@@ -872,15 +876,80 @@ class AgentDiscovery:
872
876
  try:
873
877
  mounts = Path("/proc/mounts").read_text(encoding="utf-8")
874
878
  except OSError:
875
- return False
879
+ return True
876
880
  for line in mounts.splitlines():
877
881
  fields = line.split()
878
882
  if len(fields) >= 3 and fields[2] in {"9p", "drvfs"}:
879
- mount = Path(fields[1].replace("\\040", " "))
883
+ mount = Path(re.sub(r"\\([0-7]{3})", lambda value: chr(int(value[1], 8)), fields[1]))
880
884
  if resolved == mount or mount in resolved.parents:
881
885
  return True
882
886
  return False
883
887
 
888
+ @staticmethod
889
+ def _wsl_execution_issue(
890
+ path: str, env: dict[str, str] | None = None,
891
+ ) -> str | None:
892
+ """Validate the selected WSL CLI and declared interpreter without spawning.
893
+
894
+ This is same-side admission, not a sandbox for the CLI's own commands.
895
+ Native Windows and non-WSL Linux retain their existing launch behavior.
896
+ """
897
+ if sys.platform != "linux" or not os.environ.get("WSL_DISTRO_NAME"):
898
+ return None
899
+ issue = "WSL Agent requires an available Linux-side CLI and interpreter; Windows interop or unverifiable tools are refused. Install the CLI inside the selected WSL distribution."
900
+ candidate = path
901
+ seen: set[str] = set()
902
+ for _ in range(4):
903
+ if not Path(candidate).is_absolute():
904
+ return issue
905
+ if AgentDiscovery._wsl_windows_executable(candidate):
906
+ return issue
907
+ try:
908
+ resolved = Path(candidate).resolve(strict=True)
909
+ if str(resolved) in seen or not resolved.is_file():
910
+ return issue
911
+ seen.add(str(resolved))
912
+ with resolved.open("rb") as stream:
913
+ header = stream.read(512)
914
+ except (OSError, RuntimeError):
915
+ return issue
916
+ if header.startswith(b"MZ"):
917
+ return issue
918
+ if not header.startswith(b"#!"):
919
+ return None
920
+ try:
921
+ line = header.split(b"\n", 1)[0][2:].decode("utf-8", errors="strict")
922
+ parts = shlex.split(line)
923
+ except (UnicodeError, ValueError):
924
+ return issue
925
+ if not parts or not Path(parts[0]).is_absolute():
926
+ return issue
927
+ interpreter = parts[0]
928
+ if Path(interpreter).name == "env":
929
+ if AgentDiscovery._wsl_windows_executable(interpreter):
930
+ return issue
931
+ try:
932
+ with Path(interpreter).open("rb") as stream:
933
+ if stream.read(4) != b"\x7fELF":
934
+ return issue
935
+ except OSError:
936
+ return issue
937
+ arguments = parts[1:]
938
+ if arguments[:1] == ["-S"]:
939
+ arguments = arguments[1:]
940
+ if not arguments or arguments[0].startswith("-") or "=" in arguments[0]:
941
+ return issue
942
+ search_path = (env if env is not None else os.environ).get("PATH", "")
943
+ # Relative/empty PATH entries resolve against the child's cwd,
944
+ # not this host's cwd. Refuse rather than check another tool.
945
+ if not search_path or any(not Path(entry).is_absolute() for entry in search_path.split(os.pathsep)):
946
+ return issue
947
+ interpreter = shutil.which(arguments[0], path=search_path)
948
+ if not interpreter:
949
+ return issue
950
+ candidate = interpreter
951
+ return issue
952
+
884
953
  async def discover(self) -> list[DiscoveredAgent]:
885
954
  self._expand_path()
886
955
  # Probe bwrap support once at discovery time and log a clear warning
@@ -892,8 +961,9 @@ class AgentDiscovery:
892
961
  cmd = custom_path or spec["commands"][0]
893
962
  resolved = shutil.which(cmd)
894
963
  if resolved:
895
- if self._wsl_windows_executable(resolved):
896
- logger.warning("Agent %s resolved only to a Windows executable inside WSL; skipping", agent_id)
964
+ issue = await asyncio.to_thread(self._wsl_execution_issue, resolved)
965
+ if issue:
966
+ logger.warning("Agent %s has an unavailable Linux-side WSL toolchain; skipping", agent_id)
897
967
  continue
898
968
  version = await self._get_version(spec["detect"], resolved)
899
969
  if version is None and "detect_alt" in spec:
@@ -1977,10 +2047,24 @@ def claude_base_env(
1977
2047
  return env
1978
2048
 
1979
2049
 
2050
+ def _mirror_skill_directory(source: Path, destination: Path) -> None:
2051
+ if source.resolve() == destination.resolve():
2052
+ return
2053
+ try:
2054
+ if destination.is_symlink() or destination.is_file():
2055
+ destination.unlink()
2056
+ elif destination.is_dir():
2057
+ shutil.rmtree(destination)
2058
+ if source.is_dir():
2059
+ shutil.copytree(source, destination)
2060
+ except OSError as exc:
2061
+ logger.debug("Agent skill mirror skipped %s -> %s: %s", source, destination, exc)
2062
+
2063
+
1980
2064
  def prepare_claude_environment(
1981
2065
  sandbox_root: Path | None = None,
1982
2066
  ) -> dict[str, str]:
1983
- """Prepare an isolated Claude home and mirror stable configuration files.
2067
+ """Prepare an isolated Claude home and mirror configuration and skills.
1984
2068
 
1985
2069
  When *sandbox_root* is given (per-task isolation via mkdtemp), uses that
1986
2070
  directory. When None, falls back to the shared legacy path under ~/.forgexa.
@@ -1990,8 +2074,8 @@ def prepare_claude_environment(
1990
2074
  Claude use its default config, cache, and session locations under a
1991
2075
  writable task sandbox.
1992
2076
 
1993
- Mirror only stable authentication and settings files, including Claude
1994
- Code's current credential store. Runtime state such as session-env,
2077
+ Mirror stable authentication, settings and installed skill bundles, including
2078
+ Claude Code's current credential store. Runtime state such as session-env,
1995
2079
  projects, task locks, and history must never cross into an isolated daemon
1996
2080
  invocation. Do not set CLAUDE_CODE_DEBUG_LOGS_DIR:
1997
2081
  Claude Code 2.1.63 treats a directory supplied through that variable as
@@ -2028,6 +2112,9 @@ def prepare_claude_environment(
2028
2112
  source, dest, exc,
2029
2113
  )
2030
2114
 
2115
+ for relative_root in (Path(".claude/skills"), Path(".agents/skills")):
2116
+ _mirror_skill_directory(Path.home() / relative_root, sandbox_root / relative_root)
2117
+
2031
2118
  env = {
2032
2119
  "HOME": str(sandbox_root),
2033
2120
  "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": str(
@@ -2375,7 +2462,7 @@ def deepseek_harness_build_info(binary: str) -> dict:
2375
2462
  if result.returncode or not re.fullmatch(r"\d+\.\d+\.\d+", node_version) or parsed is None or not ((parsed[0] == 22 and parsed >= (22, 19, 0)) or parsed[0] >= 24):
2376
2463
  raise RuntimeError("DeepSeek Harness requires Node 22.19+ or Node 24+")
2377
2464
  digest = hashlib.sha256()
2378
- digest.update(f"{sys.platform}:{platform.machine()}:{platform.release()}:{node_version}:harness-policy-2".encode())
2465
+ digest.update(f"{sys.platform}:{platform.machine()}:{platform.release()}:{node_version}:harness-policy-3".encode())
2379
2466
  extensions = [manifest.parent for manifest in (roots[0] / "node_modules" / "@deepseek-ai").glob("dsh*/package.json")]
2380
2467
  for root in sorted(set([*roots, *extensions])):
2381
2468
  files = [root / "package.json"] + sorted((root / "lib").rglob("*.js"))
@@ -2500,10 +2587,15 @@ def deepseek_harness_overlay(*, cwd: Path, credentials_path: Path, provider: str
2500
2587
  {"id": "tools", "config": {"mode": "native"}},
2501
2588
  {"id": "session-log-deepseek", "config": {"enabled": False}},
2502
2589
  {"id": "plugin-package-inventory-deepseek", "config": {"enabled": False}},
2590
+ {"id": "skill-filesystem", "disabled": False, "config": {
2591
+ "dshHome": str(Path(os.environ.get("DSH_HOME", str(Path.home() / ".dsh"))).expanduser().resolve()),
2592
+ "watch": False,
2593
+ }},
2594
+ {"id": "tool-skill", "disabled": False},
2503
2595
  ]
2504
2596
  disabled = (
2505
2597
  "session-telemetry-otel", "hmr", "plugin-manager", "tool-plugin-manager",
2506
- "llm-pi-ai", "skill-filesystem", "tool-skill", "tool-subagent", "tool-subagent-fork",
2598
+ "llm-pi-ai", "tool-subagent", "tool-subagent-fork",
2507
2599
  "tool-subagent-control", "tool-subagent-list-agents", "tool-workflow",
2508
2600
  "goal-round-driver", "command-goal", "tool-goal", "command-feedback", "tool-web",
2509
2601
  )
@@ -2748,7 +2840,7 @@ def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
2748
2840
  "MIMOCODE_PURE": "true",
2749
2841
  "MIMOCODE_DISABLE_DEFAULT_PLUGINS": "true",
2750
2842
  "MIMOCODE_DISABLE_PROJECT_CONFIG": "true",
2751
- "MIMOCODE_DISABLE_EXTERNAL_SKILLS": "true",
2843
+ "MIMOCODE_DISABLE_EXTERNAL_SKILLS": "false",
2752
2844
  "MIMOCODE_DISABLE_LSP_DOWNLOAD": "true",
2753
2845
  "MIMOCODE_DISABLE_CLAUDE_CODE_MCP": "true",
2754
2846
  "MIMOCODE_DISABLE_AUTOUPDATE": "true",
@@ -2771,7 +2863,7 @@ def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
2771
2863
  },
2772
2864
  "external_directory": "deny",
2773
2865
  "task": "deny",
2774
- "skill": "deny",
2866
+ "skill": {"*": "allow", "compose:*": "deny"},
2775
2867
  "webfetch": "deny",
2776
2868
  "websearch": "deny",
2777
2869
  "codesearch": "deny",
@@ -2886,16 +2978,16 @@ def build_qwen_command(
2886
2978
  ) -> list[str]:
2887
2979
  """Run Qwen Code headlessly with its user-managed local profile.
2888
2980
 
2889
- Qwen Code owns authentication, endpoint, and model configuration. Safe
2890
- mode disables project/user customizations for this Forgexa invocation
2891
- without replacing the user's native profile.
2981
+ Qwen Code owns authentication, endpoint, model and skill discovery. The
2982
+ environment policy disables hooks and MCP; CLI overrides disable extensions
2983
+ without suppressing native skills or replacing the user's profile.
2892
2984
  """
2893
2985
  cmd = [
2894
2986
  binary,
2895
2987
  "--output-format", "stream-json",
2896
2988
  "--include-partial-messages",
2897
2989
  "--approval-mode", approval_mode,
2898
- "--safe-mode",
2990
+ "--extensions", "",
2899
2991
  ]
2900
2992
  if sandbox:
2901
2993
  cmd.append("--sandbox")
@@ -2971,6 +3063,9 @@ def zcode_base_env(env: dict[str, str]) -> dict[str, str]:
2971
3063
  storage.chmod(0o700)
2972
3064
  except OSError as exc:
2973
3065
  raise RuntimeError(f"could not prepare isolated ZCode profile: {exc}") from exc
3066
+ source_home = Path(source_env.get("HOME") or source_env.get("USERPROFILE") or str(Path.home())).expanduser()
3067
+ for relative_root in (Path(".zcode/skills"), Path(".agents/skills")):
3068
+ _mirror_skill_directory(source_home / relative_root, root / relative_root)
2974
3069
  safe_keys = {
2975
3070
  "PATH", "PATHEXT", "SystemRoot", "WINDIR", "COMSPEC",
2976
3071
  "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "LC_CTYPE", "TERM",
@@ -3073,9 +3168,9 @@ def zcode_build_verification_issue(binary: str | Path, env: dict[str, str] | Non
3073
3168
  return None
3074
3169
 
3075
3170
 
3076
- ZCODE_ALLOWED_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write")
3171
+ ZCODE_ALLOWED_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write", "Skill")
3077
3172
  ZCODE_DENIED_TOOLS = (
3078
- "Bash", "MCP", "Plugin", "Skill", "Task", "WebFetch", "WebSearch",
3173
+ "Bash", "MCP", "Plugin", "Task", "WebFetch", "WebSearch",
3079
3174
  )
3080
3175
 
3081
3176
 
@@ -3152,11 +3247,10 @@ def agent_execution_verification_issue(agent_id: str, binary: str) -> str | None
3152
3247
  return None
3153
3248
 
3154
3249
 
3155
- _FORGEXA_CODING_AGENT_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write", "Bash")
3250
+ _FORGEXA_CODING_AGENT_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write", "Bash", "Skill")
3156
3251
  _FORGEXA_CODING_AGENT_DENIED_TOOLS = (
3157
3252
  "Agent",
3158
3253
  "Task",
3159
- "Skill",
3160
3254
  "SlashCommand",
3161
3255
  "WebFetch",
3162
3256
  "WebSearch",
@@ -3229,8 +3323,8 @@ def build_qoder_command(
3229
3323
  """Run Qoder CLI through its isolated headless stream-json protocol.
3230
3324
 
3231
3325
  Qoder receives model selection through its official ``QODER_MODEL``
3232
- environment variable. All settings sources stay disabled so user, project,
3233
- and local hooks, plugins, and skills cannot affect a Forgexa task.
3326
+ environment variable. Settings sources stay disabled to isolate hooks and
3327
+ plugins; installed skills remain available through the managed profile.
3234
3328
  """
3235
3329
  cmd = [
3236
3330
  binary,
@@ -3382,7 +3476,7 @@ def _link_qoder_profile_entry(
3382
3476
 
3383
3477
 
3384
3478
  def qoder_managed_profile_dir(env: dict[str, str] | None = None) -> Path:
3385
- """Create a stable Qoder profile containing only auth and model metadata."""
3479
+ """Create a stable Qoder profile containing auth, model metadata and skills."""
3386
3480
  source_env = os.environ if env is None else env
3387
3481
  source_root = _qoder_source_profile_root(source_env)
3388
3482
  managed_root = _qoder_managed_profile_root(source_root)
@@ -3393,13 +3487,13 @@ def qoder_managed_profile_dir(env: dict[str, str] | None = None) -> Path:
3393
3487
  except OSError as exc:
3394
3488
  raise RuntimeError(f"could not create managed Qoder profile: {exc}") from exc
3395
3489
 
3396
- for name in (".auth", ".models", "installation_id"):
3490
+ for name in (".auth", ".models", "installation_id", "skills"):
3397
3491
  _link_qoder_profile_entry(source_root, managed_root, name)
3398
3492
  return managed_root
3399
3493
 
3400
3494
 
3401
3495
  def qoder_base_env(env: dict[str, str]) -> dict[str, str]:
3402
- """Constrain Qoder while linking only user-managed auth and model metadata."""
3496
+ """Constrain Qoder while preserving user-managed auth, model and skills."""
3403
3497
  env = dict(env)
3404
3498
  local_model = qoder_local_model(env)
3405
3499
  managed_profile = qoder_managed_profile_dir(env)
@@ -3425,9 +3519,8 @@ def qwen_base_env(env: dict[str, str]) -> dict[str, str]:
3425
3519
  """Return a constrained environment without replacing Qwen's local profile.
3426
3520
 
3427
3521
  Authentication, endpoint, and model selection remain entirely under the
3428
- user's Qwen Code configuration. The command's native ``--safe-mode`` flag
3429
- handles customization isolation; this function only disables telemetry for
3430
- the Forgexa-managed process.
3522
+ user's Qwen Code configuration. A system override isolates executable
3523
+ customizations without disabling installed skills.
3431
3524
  """
3432
3525
  env = dict(env)
3433
3526
  for key in (
@@ -3439,7 +3532,23 @@ def qwen_base_env(env: dict[str, str]) -> dict[str, str]:
3439
3532
  "QWEN_CODE_SYSTEM_SETTINGS_PATH",
3440
3533
  ):
3441
3534
  env.pop(key, None)
3535
+ settings_dir = Path.home() / ".forgexa" / "qwen"
3536
+ settings_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
3537
+ settings_path = settings_dir / "settings.json"
3538
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=settings_dir, delete=False) as temporary:
3539
+ json.dump({
3540
+ "disableAllHooks": True,
3541
+ "mcp": {"excluded": ["*"], "serverCommand": ""},
3542
+ "tools": {"discoveryCommand": "", "callCommand": ""},
3543
+ "skills": {"disabledLevels": ["extension"]},
3544
+ }, temporary)
3545
+ temporary_path = Path(temporary.name)
3546
+ try:
3547
+ os.replace(temporary_path, settings_path)
3548
+ finally:
3549
+ temporary_path.unlink(missing_ok=True)
3442
3550
  env.update({
3551
+ "QWEN_CODE_SYSTEM_SETTINGS_PATH": str(settings_path),
3443
3552
  "QWEN_USAGE_STATISTICS_ENABLED": "0",
3444
3553
  "QWEN_TELEMETRY_ENABLED": "0",
3445
3554
  "QWEN_TELEMETRY_LOG_PROMPTS": "0",
@@ -1196,7 +1196,7 @@ except (ImportError, ModuleNotFoundError):
1196
1196
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1197
1197
  # Kept in sync with pyproject.toml version via bump-version.sh.
1198
1198
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1199
- DAEMON_VERSION = "1.62.4"
1199
+ DAEMON_VERSION = "1.62.5"
1200
1200
 
1201
1201
 
1202
1202
  def _detect_client_type() -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.62.4
3
+ Version: 1.62.5
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.62.4"
3
+ version = "1.62.5"
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