forgexa-cli 1.62.4__tar.gz → 1.63.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 (29) hide show
  1. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/agent_core.py +152 -36
  4. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/daemon.py +31 -23
  5. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/pyproject.toml +1 -1
  7. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/README.md +0 -0
  8. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/setup.cfg +0 -0
  21. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.62.4 → forgexa_cli-1.63.0}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.62.4 → forgexa_cli-1.63.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.62.4
3
+ Version: 1.63.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.62.4"
2
+ __version__ = "1.63.0"
@@ -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:
@@ -1239,12 +1309,14 @@ def _kill_proc(proc: asyncio.subprocess.Process) -> None:
1239
1309
  (npm, yarn, ssh, git, etc.) spawned by the agent stay alive, keep pipes
1240
1310
  open, and exhaust system resources. ``os.killpg`` sends SIGKILL to the
1241
1311
  whole process group, reliably cleaning up all descendants.
1312
+ Owned agents start a new session, so their PID remains the process-group
1313
+ ID even after the leader exits.
1242
1314
  """
1243
1315
  try:
1244
1316
  if sys.platform != "win32":
1245
1317
  import signal as _signal
1246
1318
  try:
1247
- os.killpg(os.getpgid(proc.pid), _signal.SIGKILL)
1319
+ os.killpg(proc.pid, _signal.SIGKILL)
1248
1320
  except (ProcessLookupError, PermissionError, OSError):
1249
1321
  pass
1250
1322
  else:
@@ -1252,6 +1324,7 @@ def _kill_proc(proc: asyncio.subprocess.Process) -> None:
1252
1324
  _subprocess.run(
1253
1325
  ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
1254
1326
  capture_output=True,
1327
+ timeout=5,
1255
1328
  **_agent_process_group_kwargs(),
1256
1329
  )
1257
1330
  except Exception:
@@ -1977,10 +2050,24 @@ def claude_base_env(
1977
2050
  return env
1978
2051
 
1979
2052
 
2053
+ def _mirror_skill_directory(source: Path, destination: Path) -> None:
2054
+ if source.resolve() == destination.resolve():
2055
+ return
2056
+ try:
2057
+ if destination.is_symlink() or destination.is_file():
2058
+ destination.unlink()
2059
+ elif destination.is_dir():
2060
+ shutil.rmtree(destination)
2061
+ if source.is_dir():
2062
+ shutil.copytree(source, destination)
2063
+ except OSError as exc:
2064
+ logger.debug("Agent skill mirror skipped %s -> %s: %s", source, destination, exc)
2065
+
2066
+
1980
2067
  def prepare_claude_environment(
1981
2068
  sandbox_root: Path | None = None,
1982
2069
  ) -> dict[str, str]:
1983
- """Prepare an isolated Claude home and mirror stable configuration files.
2070
+ """Prepare an isolated Claude home and mirror configuration and skills.
1984
2071
 
1985
2072
  When *sandbox_root* is given (per-task isolation via mkdtemp), uses that
1986
2073
  directory. When None, falls back to the shared legacy path under ~/.forgexa.
@@ -1990,8 +2077,8 @@ def prepare_claude_environment(
1990
2077
  Claude use its default config, cache, and session locations under a
1991
2078
  writable task sandbox.
1992
2079
 
1993
- Mirror only stable authentication and settings files, including Claude
1994
- Code's current credential store. Runtime state such as session-env,
2080
+ Mirror stable authentication, settings and installed skill bundles, including
2081
+ Claude Code's current credential store. Runtime state such as session-env,
1995
2082
  projects, task locks, and history must never cross into an isolated daemon
1996
2083
  invocation. Do not set CLAUDE_CODE_DEBUG_LOGS_DIR:
1997
2084
  Claude Code 2.1.63 treats a directory supplied through that variable as
@@ -2028,6 +2115,9 @@ def prepare_claude_environment(
2028
2115
  source, dest, exc,
2029
2116
  )
2030
2117
 
2118
+ for relative_root in (Path(".claude/skills"), Path(".agents/skills")):
2119
+ _mirror_skill_directory(Path.home() / relative_root, sandbox_root / relative_root)
2120
+
2031
2121
  env = {
2032
2122
  "HOME": str(sandbox_root),
2033
2123
  "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": str(
@@ -2375,7 +2465,7 @@ def deepseek_harness_build_info(binary: str) -> dict:
2375
2465
  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
2466
  raise RuntimeError("DeepSeek Harness requires Node 22.19+ or Node 24+")
2377
2467
  digest = hashlib.sha256()
2378
- digest.update(f"{sys.platform}:{platform.machine()}:{platform.release()}:{node_version}:harness-policy-2".encode())
2468
+ digest.update(f"{sys.platform}:{platform.machine()}:{platform.release()}:{node_version}:harness-policy-4".encode())
2379
2469
  extensions = [manifest.parent for manifest in (roots[0] / "node_modules" / "@deepseek-ai").glob("dsh*/package.json")]
2380
2470
  for root in sorted(set([*roots, *extensions])):
2381
2471
  files = [root / "package.json"] + sorted((root / "lib").rglob("*.js"))
@@ -2500,10 +2590,20 @@ def deepseek_harness_overlay(*, cwd: Path, credentials_path: Path, provider: str
2500
2590
  {"id": "tools", "config": {"mode": "native"}},
2501
2591
  {"id": "session-log-deepseek", "config": {"enabled": False}},
2502
2592
  {"id": "plugin-package-inventory-deepseek", "config": {"enabled": False}},
2593
+ {"id": "skill-filesystem", "disabled": False, "config": {
2594
+ "dshHome": str(Path(os.environ.get("DSH_HOME", str(Path.home() / ".dsh"))).expanduser().resolve()),
2595
+ "watch": False,
2596
+ }},
2597
+ {"id": "tool-skill", "disabled": False},
2598
+ {"id": "tool-subagent", "disabled": False, "config": {
2599
+ "provider": "spawn", "enableRunInBackground": False,
2600
+ "backgroundMode": "one-shot", "maxDepth": 1,
2601
+ "modelSelectionSettings": False,
2602
+ }},
2503
2603
  ]
2504
2604
  disabled = (
2505
2605
  "session-telemetry-otel", "hmr", "plugin-manager", "tool-plugin-manager",
2506
- "llm-pi-ai", "skill-filesystem", "tool-skill", "tool-subagent", "tool-subagent-fork",
2606
+ "llm-pi-ai", "tool-subagent-fork",
2507
2607
  "tool-subagent-control", "tool-subagent-list-agents", "tool-workflow",
2508
2608
  "goal-round-driver", "command-goal", "tool-goal", "command-feedback", "tool-web",
2509
2609
  )
@@ -2748,7 +2848,7 @@ def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
2748
2848
  "MIMOCODE_PURE": "true",
2749
2849
  "MIMOCODE_DISABLE_DEFAULT_PLUGINS": "true",
2750
2850
  "MIMOCODE_DISABLE_PROJECT_CONFIG": "true",
2751
- "MIMOCODE_DISABLE_EXTERNAL_SKILLS": "true",
2851
+ "MIMOCODE_DISABLE_EXTERNAL_SKILLS": "false",
2752
2852
  "MIMOCODE_DISABLE_LSP_DOWNLOAD": "true",
2753
2853
  "MIMOCODE_DISABLE_CLAUDE_CODE_MCP": "true",
2754
2854
  "MIMOCODE_DISABLE_AUTOUPDATE": "true",
@@ -2770,8 +2870,8 @@ def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
2770
2870
  "git * reset --hard*": "deny",
2771
2871
  },
2772
2872
  "external_directory": "deny",
2773
- "task": "deny",
2774
- "skill": "deny",
2873
+ "task": "allow",
2874
+ "skill": {"*": "allow", "compose:*": "deny"},
2775
2875
  "webfetch": "deny",
2776
2876
  "websearch": "deny",
2777
2877
  "codesearch": "deny",
@@ -2886,16 +2986,16 @@ def build_qwen_command(
2886
2986
  ) -> list[str]:
2887
2987
  """Run Qwen Code headlessly with its user-managed local profile.
2888
2988
 
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.
2989
+ Qwen Code owns authentication, endpoint, model and skill discovery. The
2990
+ environment policy disables hooks and MCP; CLI overrides disable extensions
2991
+ without suppressing native skills or replacing the user's profile.
2892
2992
  """
2893
2993
  cmd = [
2894
2994
  binary,
2895
2995
  "--output-format", "stream-json",
2896
2996
  "--include-partial-messages",
2897
2997
  "--approval-mode", approval_mode,
2898
- "--safe-mode",
2998
+ "--extensions", "",
2899
2999
  ]
2900
3000
  if sandbox:
2901
3001
  cmd.append("--sandbox")
@@ -2971,6 +3071,9 @@ def zcode_base_env(env: dict[str, str]) -> dict[str, str]:
2971
3071
  storage.chmod(0o700)
2972
3072
  except OSError as exc:
2973
3073
  raise RuntimeError(f"could not prepare isolated ZCode profile: {exc}") from exc
3074
+ source_home = Path(source_env.get("HOME") or source_env.get("USERPROFILE") or str(Path.home())).expanduser()
3075
+ for relative_root in (Path(".zcode/skills"), Path(".agents/skills")):
3076
+ _mirror_skill_directory(source_home / relative_root, root / relative_root)
2974
3077
  safe_keys = {
2975
3078
  "PATH", "PATHEXT", "SystemRoot", "WINDIR", "COMSPEC",
2976
3079
  "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "LC_CTYPE", "TERM",
@@ -3073,9 +3176,9 @@ def zcode_build_verification_issue(binary: str | Path, env: dict[str, str] | Non
3073
3176
  return None
3074
3177
 
3075
3178
 
3076
- ZCODE_ALLOWED_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write")
3179
+ ZCODE_ALLOWED_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write", "Skill", "Agent", "Task")
3077
3180
  ZCODE_DENIED_TOOLS = (
3078
- "Bash", "MCP", "Plugin", "Skill", "Task", "WebFetch", "WebSearch",
3181
+ "Bash", "MCP", "Plugin", "WebFetch", "WebSearch",
3079
3182
  )
3080
3183
 
3081
3184
 
@@ -3152,11 +3255,8 @@ def agent_execution_verification_issue(agent_id: str, binary: str) -> str | None
3152
3255
  return None
3153
3256
 
3154
3257
 
3155
- _FORGEXA_CODING_AGENT_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write", "Bash")
3258
+ _FORGEXA_CODING_AGENT_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write", "Bash", "Skill", "Agent", "Task")
3156
3259
  _FORGEXA_CODING_AGENT_DENIED_TOOLS = (
3157
- "Agent",
3158
- "Task",
3159
- "Skill",
3160
3260
  "SlashCommand",
3161
3261
  "WebFetch",
3162
3262
  "WebSearch",
@@ -3229,8 +3329,8 @@ def build_qoder_command(
3229
3329
  """Run Qoder CLI through its isolated headless stream-json protocol.
3230
3330
 
3231
3331
  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.
3332
+ environment variable. Settings sources stay disabled to isolate hooks and
3333
+ plugins; installed skills remain available through the managed profile.
3234
3334
  """
3235
3335
  cmd = [
3236
3336
  binary,
@@ -3382,7 +3482,7 @@ def _link_qoder_profile_entry(
3382
3482
 
3383
3483
 
3384
3484
  def qoder_managed_profile_dir(env: dict[str, str] | None = None) -> Path:
3385
- """Create a stable Qoder profile containing only auth and model metadata."""
3485
+ """Create a stable Qoder profile containing auth, model metadata and skills."""
3386
3486
  source_env = os.environ if env is None else env
3387
3487
  source_root = _qoder_source_profile_root(source_env)
3388
3488
  managed_root = _qoder_managed_profile_root(source_root)
@@ -3393,13 +3493,13 @@ def qoder_managed_profile_dir(env: dict[str, str] | None = None) -> Path:
3393
3493
  except OSError as exc:
3394
3494
  raise RuntimeError(f"could not create managed Qoder profile: {exc}") from exc
3395
3495
 
3396
- for name in (".auth", ".models", "installation_id"):
3496
+ for name in (".auth", ".models", "installation_id", "skills"):
3397
3497
  _link_qoder_profile_entry(source_root, managed_root, name)
3398
3498
  return managed_root
3399
3499
 
3400
3500
 
3401
3501
  def qoder_base_env(env: dict[str, str]) -> dict[str, str]:
3402
- """Constrain Qoder while linking only user-managed auth and model metadata."""
3502
+ """Constrain Qoder while preserving user-managed auth, model and skills."""
3403
3503
  env = dict(env)
3404
3504
  local_model = qoder_local_model(env)
3405
3505
  managed_profile = qoder_managed_profile_dir(env)
@@ -3425,9 +3525,8 @@ def qwen_base_env(env: dict[str, str]) -> dict[str, str]:
3425
3525
  """Return a constrained environment without replacing Qwen's local profile.
3426
3526
 
3427
3527
  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.
3528
+ user's Qwen Code configuration. A system override isolates executable
3529
+ customizations without disabling installed skills.
3431
3530
  """
3432
3531
  env = dict(env)
3433
3532
  for key in (
@@ -3439,7 +3538,23 @@ def qwen_base_env(env: dict[str, str]) -> dict[str, str]:
3439
3538
  "QWEN_CODE_SYSTEM_SETTINGS_PATH",
3440
3539
  ):
3441
3540
  env.pop(key, None)
3541
+ settings_dir = Path.home() / ".forgexa" / "qwen"
3542
+ settings_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
3543
+ settings_path = settings_dir / "settings.json"
3544
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=settings_dir, delete=False) as temporary:
3545
+ json.dump({
3546
+ "disableAllHooks": True,
3547
+ "mcp": {"excluded": ["*"], "serverCommand": ""},
3548
+ "tools": {"discoveryCommand": "", "callCommand": ""},
3549
+ "skills": {"disabledLevels": ["extension"]},
3550
+ }, temporary)
3551
+ temporary_path = Path(temporary.name)
3552
+ try:
3553
+ os.replace(temporary_path, settings_path)
3554
+ finally:
3555
+ temporary_path.unlink(missing_ok=True)
3442
3556
  env.update({
3557
+ "QWEN_CODE_SYSTEM_SETTINGS_PATH": str(settings_path),
3443
3558
  "QWEN_USAGE_STATISTICS_ENABLED": "0",
3444
3559
  "QWEN_TELEMETRY_ENABLED": "0",
3445
3560
  "QWEN_TELEMETRY_LOG_PROMPTS": "0",
@@ -3546,9 +3661,10 @@ def build_copilot_command(
3546
3661
 
3547
3662
  ``prompt_via_stdin`` omits ``-p`` so the caller pipes the prompt into
3548
3663
  stdin instead - verified live on CLI 1.0.86, where piped stdin is a full
3549
- non-interactive prompt source. The host uses it when the argv form would
3550
- exceed the platform command-line limit (inlined attachments can push the
3551
- prompt past Windows' 32k-char CreateProcess ceiling).
3664
+ non-interactive prompt source. The daemon uses it for native launchers
3665
+ to avoid single-argument and total command-line limits; Windows batch
3666
+ wrappers retain task-file redirection. The host uses it when the argv
3667
+ form would exceed the platform command-line limit.
3552
3668
  """
3553
3669
  cmd = [
3554
3670
  binary,
@@ -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.63.0"
1200
1200
 
1201
1201
 
1202
1202
  def _detect_client_type() -> str:
@@ -6525,10 +6525,11 @@ class ProcessManager:
6525
6525
  _last_activity_at: list[float] = [time.monotonic()]
6526
6526
 
6527
6527
  # Write prompt and close stdin so the agent knows input is done.
6528
- if stdin_input and proc.stdin:
6528
+ if proc.stdin:
6529
6529
  try:
6530
- proc.stdin.write(stdin_input)
6531
- await proc.stdin.drain()
6530
+ if stdin_input:
6531
+ proc.stdin.write(stdin_input)
6532
+ await proc.stdin.drain()
6532
6533
  except Exception:
6533
6534
  pass
6534
6535
  proc.stdin.close()
@@ -7668,7 +7669,8 @@ class ProcessManager:
7668
7669
  explicitly configured supported token environment variable.
7669
7670
 
7670
7671
  Flags:
7671
- -p / --prompt Non-interactive prompt (exits after completion).
7672
+ stdin Full non-interactive prompt, avoiding argv limits.
7673
+ -p / --prompt Short task-file instruction for Windows wrappers.
7672
7674
  --output-format json JSONL stream of session events.
7673
7675
  --allow-all Grant all tool + path + URL permissions required
7674
7676
  for autonomous file-editing tasks.
@@ -7768,11 +7770,13 @@ class ProcessManager:
7768
7770
  _effective_prompt, _task_file = _redirect_prompt_through_task_file(
7769
7771
  cwd, prompt, task_id, "Copilot",
7770
7772
  )
7773
+ _stdin_input = None if _task_file is not None else prompt.encode("utf-8")
7771
7774
 
7772
7775
  cmd = agent_core.build_copilot_command(
7773
7776
  agent.command,
7774
7777
  cwd=cwd,
7775
7778
  prompt=_effective_prompt,
7779
+ prompt_via_stdin=_stdin_input is not None,
7776
7780
  model=model_override,
7777
7781
  reasoning=reasoning if not _retry_without_effort else None,
7778
7782
  disable_builtin_mcps=disable_builtin_mcps,
@@ -7786,9 +7790,20 @@ class ProcessManager:
7786
7790
  # reproduce failures manually and diagnose CLI version incompatibilities.
7787
7791
  _cmd_display = " ".join(
7788
7792
  f'"{a}"' if " " in a else a
7789
- for a in cmd[:-1] # omit the -p value (potentially huge/sensitive)
7790
- ) + ' -p "<prompt>"'
7791
- logger.debug("Copilot invocation for task %s: %s", task_id, _cmd_display)
7793
+ for a in (cmd if _stdin_input is not None else cmd[:-1])
7794
+ )
7795
+ if _stdin_input is None:
7796
+ _cmd_display += ' "<prompt>"'
7797
+ logger.debug(
7798
+ "Copilot invocation for task %s: %s (prompt via %s)",
7799
+ task_id, _cmd_display, "stdin" if _stdin_input is not None else "task file",
7800
+ )
7801
+ logger.info(
7802
+ "Copilot task %s prompt transport: %s (%d UTF-8 bytes)",
7803
+ task_id,
7804
+ "stdin" if _stdin_input is not None else "task file",
7805
+ len(_stdin_input) if _stdin_input is not None else len(prompt.encode("utf-8")),
7806
+ )
7792
7807
 
7793
7808
  # On Windows, CREATE_NO_WINDOW ensures GetConsoleWindow() returns NULL for
7794
7809
  # the child process, even when the parent (daemon or Tauri app) has a console
@@ -7796,9 +7811,9 @@ class ProcessManager:
7796
7811
  # check via GetConsoleWindow(): if a console window is visible, it uses only
7797
7812
  # the first line of -p as a brief context note and reports "No task was
7798
7813
  # provided" for the rest of the prompt — silently discarding the full task.
7799
- # stdin=DEVNULL (below) covers the primary check (GetConsoleMode on stdin);
7814
+ # A pipe or DEVNULL covers the primary check (GetConsoleMode on stdin);
7800
7815
  # CREATE_NO_WINDOW covers this secondary check so Copilot always sees a fully
7801
- # headless environment and processes -p as the complete task specification.
7816
+ # headless environment and processes the complete task specification.
7802
7817
  proc: asyncio.subprocess.Process | None = None
7803
7818
 
7804
7819
  async def _terminate_copilot_process(
@@ -7817,18 +7832,11 @@ class ProcessManager:
7817
7832
  *cmd,
7818
7833
  stdout=asyncio.subprocess.PIPE,
7819
7834
  stderr=asyncio.subprocess.PIPE,
7820
- # stdin=DEVNULL is critical on Windows.
7821
- # Without it, the child process inherits the parent daemon's stdin.
7822
- # On Windows, the daemon often has a real console handle on stdin
7823
- # (e.g. from a CMD/PowerShell window or the Desktop app's console).
7824
- # Copilot CLI detects this via GetConsoleMode(GetStdHandle(STD_INPUT))
7825
- # and enters interactive/session-check mode, ignoring -p as a task:
7826
- # it checks the session todo table (empty), reports "no tracked work
7827
- # items", and waits for user input instead of executing the task.
7828
- # On Linux/macOS (daemon as service), stdin is typically /dev/null so
7829
- # the bug doesn't manifest. Explicit DEVNULL ensures non-interactive
7830
- # mode on all platforms, matching the intended headless operation.
7831
- stdin=asyncio.subprocess.DEVNULL,
7835
+ stdin=(
7836
+ asyncio.subprocess.PIPE
7837
+ if _stdin_input is not None
7838
+ else asyncio.subprocess.DEVNULL
7839
+ ),
7832
7840
  cwd=str(cwd),
7833
7841
  env=env,
7834
7842
  limit=100 * 1024 * 1024,
@@ -7836,7 +7844,7 @@ class ProcessManager:
7836
7844
  )
7837
7845
  self.active_processes[task_id] = proc
7838
7846
  stdout, stderr, returncode = await self._stream_process(
7839
- proc, None, timeout, task_id, on_chunk,
7847
+ proc, _stdin_input, timeout, task_id, on_chunk,
7840
7848
  workspace_path=cwd,
7841
7849
  stream_stderr=stream_stderr,
7842
7850
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.62.4
3
+ Version: 1.63.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.62.4"
3
+ version = "1.63.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