fixos 2.2.20__tar.gz → 2.2.22__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 (142) hide show
  1. {fixos-2.2.20 → fixos-2.2.22}/CHANGELOG.md +23 -0
  2. {fixos-2.2.20 → fixos-2.2.22}/PKG-INFO +5 -5
  3. {fixos-2.2.20 → fixos-2.2.22}/README.md +4 -4
  4. {fixos-2.2.20 → fixos-2.2.22}/fixos/__init__.py +1 -1
  5. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/session_core.py +20 -1
  6. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/session_handlers.py +48 -3
  7. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/session_io.py +6 -6
  8. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/ask_cmd.py +6 -5
  9. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/cleanup_cmd.py +33 -24
  10. {fixos-2.2.20 → fixos-2.2.22}/fixos/platform_utils.py +18 -0
  11. {fixos-2.2.20 → fixos-2.2.22}/fixos.egg-info/PKG-INFO +5 -5
  12. {fixos-2.2.20 → fixos-2.2.22}/fixos.egg-info/top_level.txt +1 -0
  13. {fixos-2.2.20 → fixos-2.2.22}/pyproject.toml +1 -1
  14. {fixos-2.2.20 → fixos-2.2.22}/setup.py +1 -1
  15. {fixos-2.2.20 → fixos-2.2.22}/.env.example +0 -0
  16. {fixos-2.2.20 → fixos-2.2.22}/LICENSE +0 -0
  17. {fixos-2.2.20 → fixos-2.2.22}/MANIFEST.in +0 -0
  18. {fixos-2.2.20 → fixos-2.2.22}/docker/README.md +0 -0
  19. {fixos-2.2.20 → fixos-2.2.22}/docker/TEST_RESULTS.md +0 -0
  20. {fixos-2.2.20 → fixos-2.2.22}/docker/TEST_RESULTS_V2.md +0 -0
  21. {fixos-2.2.20 → fixos-2.2.22}/docker/alpine/Dockerfile +0 -0
  22. {fixos-2.2.20 → fixos-2.2.22}/docker/arch/Dockerfile +0 -0
  23. {fixos-2.2.20 → fixos-2.2.22}/docker/base/Dockerfile +0 -0
  24. {fixos-2.2.20 → fixos-2.2.22}/docker/broken-audio/Dockerfile +0 -0
  25. {fixos-2.2.20 → fixos-2.2.22}/docker/broken-full/Dockerfile +0 -0
  26. {fixos-2.2.20 → fixos-2.2.22}/docker/broken-network/Dockerfile +0 -0
  27. {fixos-2.2.20 → fixos-2.2.22}/docker/broken-thumbnails/Dockerfile +0 -0
  28. {fixos-2.2.20 → fixos-2.2.22}/docker/debian/Dockerfile +0 -0
  29. {fixos-2.2.20 → fixos-2.2.22}/docker/docker-compose.multi-system.yml +0 -0
  30. {fixos-2.2.20 → fixos-2.2.22}/docker/docker-compose.yml +0 -0
  31. {fixos-2.2.20 → fixos-2.2.22}/docker/fedora/Dockerfile +0 -0
  32. {fixos-2.2.20 → fixos-2.2.22}/docker/test-multi-system.sh +0 -0
  33. {fixos-2.2.20 → fixos-2.2.22}/docker/ubuntu/Dockerfile +0 -0
  34. {fixos-2.2.20 → fixos-2.2.22}/docs/examples/advanced_usage.py +0 -0
  35. {fixos-2.2.20 → fixos-2.2.22}/docs/examples/quickstart.py +0 -0
  36. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/__init__.py +0 -0
  37. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/autonomous.py +0 -0
  38. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/autonomous_session.py +0 -0
  39. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/hitl.py +0 -0
  40. {fixos-2.2.20 → fixos-2.2.22}/fixos/agent/hitl_session.py +0 -0
  41. {fixos-2.2.20 → fixos-2.2.22}/fixos/anonymizer.py +0 -0
  42. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/__init__.py +0 -0
  43. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/config_cmd.py +0 -0
  44. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/features_cmd.py +0 -0
  45. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/fix_cmd.py +0 -0
  46. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/history_cmd.py +0 -0
  47. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/main.py +0 -0
  48. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/orchestrate_cmd.py +0 -0
  49. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/profile_cmd.py +0 -0
  50. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/provider_cmd.py +0 -0
  51. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/quickfix_cmd.py +0 -0
  52. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/report_cmd.py +0 -0
  53. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/rollback_cmd.py +0 -0
  54. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/scan_cmd.py +0 -0
  55. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/shared.py +0 -0
  56. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/token_cmd.py +0 -0
  57. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli/watch_cmd.py +0 -0
  58. {fixos-2.2.20 → fixos-2.2.22}/fixos/cli.py +0 -0
  59. {fixos-2.2.20 → fixos-2.2.22}/fixos/config.py +0 -0
  60. {fixos-2.2.20 → fixos-2.2.22}/fixos/config_interactive.py +0 -0
  61. {fixos-2.2.20 → fixos-2.2.22}/fixos/constants.py +0 -0
  62. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/__init__.py +0 -0
  63. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/__init__.py +0 -0
  64. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/_shared.py +0 -0
  65. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/audio.py +0 -0
  66. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/hardware.py +0 -0
  67. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/resources.py +0 -0
  68. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/security.py +0 -0
  69. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/system_core.py +0 -0
  70. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/checks/thumbnails.py +0 -0
  71. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  72. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/disk_analyzer.py +0 -0
  73. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  74. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/service_cleanup.py +0 -0
  75. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/service_details.py +0 -0
  76. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/service_scanner.py +0 -0
  77. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/storage_analyzer.py +0 -0
  78. {fixos-2.2.20 → fixos-2.2.22}/fixos/diagnostics/system_checks.py +0 -0
  79. {fixos-2.2.20 → fixos-2.2.22}/fixos/features/__init__.py +0 -0
  80. {fixos-2.2.20 → fixos-2.2.22}/fixos/features/auditor.py +0 -0
  81. {fixos-2.2.20 → fixos-2.2.22}/fixos/features/catalog.py +0 -0
  82. {fixos-2.2.20 → fixos-2.2.22}/fixos/features/installer.py +0 -0
  83. {fixos-2.2.20 → fixos-2.2.22}/fixos/features/profiles.py +0 -0
  84. {fixos-2.2.20 → fixos-2.2.22}/fixos/features/renderer.py +0 -0
  85. {fixos-2.2.20 → fixos-2.2.22}/fixos/fixes/__init__.py +0 -0
  86. {fixos-2.2.20 → fixos-2.2.22}/fixos/interactive/__init__.py +0 -0
  87. {fixos-2.2.20 → fixos-2.2.22}/fixos/interactive/cleanup_planner.py +0 -0
  88. {fixos-2.2.20 → fixos-2.2.22}/fixos/llm_shell.py +0 -0
  89. {fixos-2.2.20 → fixos-2.2.22}/fixos/orchestrator/__init__.py +0 -0
  90. {fixos-2.2.20 → fixos-2.2.22}/fixos/orchestrator/executor.py +0 -0
  91. {fixos-2.2.20 → fixos-2.2.22}/fixos/orchestrator/graph.py +0 -0
  92. {fixos-2.2.20 → fixos-2.2.22}/fixos/orchestrator/orchestrator.py +0 -0
  93. {fixos-2.2.20 → fixos-2.2.22}/fixos/orchestrator/rollback.py +0 -0
  94. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/__init__.py +0 -0
  95. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/base.py +0 -0
  96. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/builtin/__init__.py +0 -0
  97. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/builtin/audio.py +0 -0
  98. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/builtin/disk.py +0 -0
  99. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/builtin/hardware.py +0 -0
  100. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/builtin/resources.py +0 -0
  101. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/builtin/security.py +0 -0
  102. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/builtin/thumbnails.py +0 -0
  103. {fixos-2.2.20 → fixos-2.2.22}/fixos/plugins/registry.py +0 -0
  104. {fixos-2.2.20 → fixos-2.2.22}/fixos/profiles/__init__.py +0 -0
  105. {fixos-2.2.20 → fixos-2.2.22}/fixos/providers/__init__.py +0 -0
  106. {fixos-2.2.20 → fixos-2.2.22}/fixos/providers/llm.py +0 -0
  107. {fixos-2.2.20 → fixos-2.2.22}/fixos/providers/llm_analyzer.py +0 -0
  108. {fixos-2.2.20 → fixos-2.2.22}/fixos/providers/schemas.py +0 -0
  109. {fixos-2.2.20 → fixos-2.2.22}/fixos/system_checks.py +0 -0
  110. {fixos-2.2.20 → fixos-2.2.22}/fixos/utils/__init__.py +0 -0
  111. {fixos-2.2.20 → fixos-2.2.22}/fixos/utils/anonymizer.py +0 -0
  112. {fixos-2.2.20 → fixos-2.2.22}/fixos/utils/terminal.py +0 -0
  113. {fixos-2.2.20 → fixos-2.2.22}/fixos/utils/timeout.py +0 -0
  114. {fixos-2.2.20 → fixos-2.2.22}/fixos/utils/web_search.py +0 -0
  115. {fixos-2.2.20 → fixos-2.2.22}/fixos/watch.py +0 -0
  116. {fixos-2.2.20 → fixos-2.2.22}/fixos.egg-info/SOURCES.txt +0 -0
  117. {fixos-2.2.20 → fixos-2.2.22}/fixos.egg-info/dependency_links.txt +0 -0
  118. {fixos-2.2.20 → fixos-2.2.22}/fixos.egg-info/entry_points.txt +0 -0
  119. {fixos-2.2.20 → fixos-2.2.22}/fixos.egg-info/requires.txt +0 -0
  120. {fixos-2.2.20 → fixos-2.2.22}/pytest.ini +0 -0
  121. {fixos-2.2.20 → fixos-2.2.22}/requirements-dev.txt +0 -0
  122. {fixos-2.2.20 → fixos-2.2.22}/requirements.txt +0 -0
  123. {fixos-2.2.20 → fixos-2.2.22}/scripts/pyqual-calibrate.py +0 -0
  124. {fixos-2.2.20 → fixos-2.2.22}/setup.cfg +0 -0
  125. {fixos-2.2.20 → fixos-2.2.22}/tests/__init__.py +0 -0
  126. {fixos-2.2.20 → fixos-2.2.22}/tests/conftest.py +0 -0
  127. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/__init__.py +0 -0
  128. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/test_anonymization_layers.py +0 -0
  129. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/test_audio_broken.py +0 -0
  130. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/test_cli.py +0 -0
  131. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/test_executor.py +0 -0
  132. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/test_multi_system.py +0 -0
  133. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/test_network_broken.py +0 -0
  134. {fixos-2.2.20 → fixos-2.2.22}/tests/e2e/test_thumbnails_broken.py +0 -0
  135. {fixos-2.2.20 → fixos-2.2.22}/tests/test_fixos.py +0 -0
  136. {fixos-2.2.20 → fixos-2.2.22}/tests/unit/__init__.py +0 -0
  137. {fixos-2.2.20 → fixos-2.2.22}/tests/unit/test_anonymizer.py +0 -0
  138. {fixos-2.2.20 → fixos-2.2.22}/tests/unit/test_core.py +0 -0
  139. {fixos-2.2.20 → fixos-2.2.22}/tests/unit/test_executor.py +0 -0
  140. {fixos-2.2.20 → fixos-2.2.22}/tests/unit/test_orchestrator.py +0 -0
  141. {fixos-2.2.20 → fixos-2.2.22}/tests/unit/test_service_cleanup.py +0 -0
  142. {fixos-2.2.20 → fixos-2.2.22}/tests/unit/test_service_scanner.py +0 -0
@@ -150,6 +150,29 @@ fix(goal): code analysis engine
150
150
  - **refactor(cli):** Usunięto zduplikowany kod ujednolicając funkcje analizy dysku do wspólnego helpera `_run_disk_analysis`.
151
151
  - **refactor(ui):** Usunięto ikony Unicode z CLI i sformatowano wyjście `stderr` oraz standardowego logowania na czysty kod Markdown dla poprawy czytelności w oknach terminalowych.
152
152
 
153
+ ## [2.2.22] - 2026-05-04
154
+
155
+ ### Docs
156
+ - Update README.md
157
+ - Update REFACTORING_PROGRESS.md
158
+
159
+ ### Other
160
+ - Update fixos/agent/session_core.py
161
+ - Update fixos/agent/session_handlers.py
162
+ - Update fixos/agent/session_io.py
163
+ - Update fixos/cli/ask_cmd.py
164
+ - Update fixos/cli/cleanup_cmd.py
165
+ - Update fixos/platform_utils.py
166
+ - Update uv.lock
167
+
168
+ ## [2.2.21] - 2026-05-04
169
+
170
+ ### Docs
171
+ - Update README.md
172
+
173
+ ### Other
174
+ - Update uv.lock
175
+
153
176
  ## [2.2.20] - 2026-05-04
154
177
 
155
178
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.20
3
+ Version: 2.2.22
4
4
  Summary: AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues
5
5
  Home-page: https://github.com/wronai/fixos
6
6
  Author: fixos contributors
@@ -63,11 +63,11 @@ AI-powered OS Diagnostics
63
63
 
64
64
  ## AI Cost Tracking
65
65
 
66
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.20-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-22.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
66
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-22.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
68
68
 
69
- - 🤖 **LLM usage:** $7.5000 (119 commits)
70
- - 👤 **Human dev:** ~$2223 (22.2h @ $100/h, 30min dedup)
69
+ - 🤖 **LLM usage:** $7.5000 (121 commits)
70
+ - 👤 **Human dev:** ~$2280 (22.8h @ $100/h, 30min dedup)
71
71
 
72
72
  Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
73
73
 
@@ -19,11 +19,11 @@ AI-powered OS Diagnostics
19
19
 
20
20
  ## AI Cost Tracking
21
21
 
22
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.20-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
23
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-22.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
22
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
23
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-22.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
24
24
 
25
- - 🤖 **LLM usage:** $7.5000 (119 commits)
26
- - 👤 **Human dev:** ~$2223 (22.2h @ $100/h, 30min dedup)
25
+ - 🤖 **LLM usage:** $7.5000 (121 commits)
26
+ - 👤 **Human dev:** ~$2280 (22.8h @ $100/h, 30min dedup)
27
27
 
28
28
  Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
29
29
 
@@ -1,2 +1,2 @@
1
1
  """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
- __version__ = "2.2.20"
2
+ __version__ = "2.2.22"
@@ -47,6 +47,8 @@ IMPORTANT RULES:
47
47
  - Do NOT use read-only diagnostics as fixes (e.g. `df -h`, `free -h`, `ls`, `cat`, `grep`, `systemctl status`).
48
48
  - If needed, mention diagnostics in explanation, but propose executable repair steps in `Komenda`.
49
49
  - For package upgrades and heavy operations, provide the real fix command (e.g. `dnf upgrade -y`).
50
+ - When disk usage is critically high (>90%), ALWAYS propose cleanup commands FIRST.
51
+ - NEVER suggest package upgrades or installations BEFORE cleanup has freed sufficient space and been verified.
50
52
 
51
53
  Always end with:
52
54
  ━━━ DOSTĘPNE AKCJE ━━━
@@ -63,10 +65,27 @@ IMPORTANT: Adapt commands to the detected OS (Linux/Windows/macOS).
63
65
 
64
66
  def _is_diagnostic_only_command(cmd: str) -> bool:
65
67
  """Return True if command is read-only and not a repair action."""
66
- normalized = cmd.strip().lower()
68
+ # Split by common shell delimiters to check each part
69
+ parts = re.split(r' && | \|\| |; ', cmd)
70
+
71
+ # If any part of a compound command looks like a repair, the whole thing is actionable
72
+ for part in parts:
73
+ if not _is_part_diagnostic_only(part):
74
+ return False
75
+ return True
76
+
77
+
78
+ def _is_part_diagnostic_only(part: str) -> bool:
79
+ """Helper for _is_diagnostic_only_command to check a single command part."""
80
+ normalized = part.strip().lower()
67
81
  if normalized.startswith("sudo "):
68
82
  normalized = normalized[5:].strip()
69
83
 
84
+ # Special case: diagnostic tools used for cleanup/repair
85
+ if normalized.startswith("journalctl"):
86
+ if "--vacuum-" in normalized or "--flush" in normalized or "--rotate" in normalized:
87
+ return False
88
+
70
89
  diagnostic_prefixes = (
71
90
  "df ",
72
91
  "free ",
@@ -13,7 +13,7 @@ from ..constants import (
13
13
  LONG_COMMAND_TIMEOUT,
14
14
  )
15
15
  from ..platform_utils import (
16
- is_dangerous, elevate_cmd, run_command,
16
+ is_dangerous, is_interactive_blocker, elevate_cmd, run_command,
17
17
  )
18
18
  from ..utils.anonymizer import anonymize
19
19
  from ..utils.web_search import search_all, format_results_for_llm
@@ -84,6 +84,34 @@ def handle_describe_problem(messages: list, ask_fn) -> bool:
84
84
  return True
85
85
 
86
86
 
87
+ def _sort_fixes_by_priority(fixes: list) -> list:
88
+ """Move cleanup commands before disk-consuming operations."""
89
+ cleanup_patterns = (
90
+ r"journalctl.*--vacuum",
91
+ r"dnf\s+(remove|autoremove|clean)",
92
+ r"apt\s+(autoremove|clean)",
93
+ r"pacman\s+-Sc",
94
+ r"rm\s+-[rf]",
95
+ r"swapoff",
96
+ )
97
+ disk_hungry_patterns = (
98
+ r"\bdnf\s+(upgrade|update|distro-sync|install)\b",
99
+ r"\bapt(-get)?\s+(upgrade|install|full-upgrade)\b",
100
+ r"\bpacman\s+-S[yuy]*\b",
101
+ r"\bflatpak\s+(update|install)\b",
102
+ )
103
+
104
+ def score(item):
105
+ cmd = item[0].lower()
106
+ if any(re.search(p, cmd) for p in cleanup_patterns):
107
+ return 0
108
+ if any(re.search(p, cmd) for p in disk_hungry_patterns):
109
+ return 2
110
+ return 1
111
+
112
+ return sorted(fixes, key=score)
113
+
114
+
87
115
  def handle_execute_all(
88
116
  fixes: list,
89
117
  messages: list,
@@ -94,7 +122,8 @@ def handle_execute_all(
94
122
  if not fixes:
95
123
  io.print_no_commands()
96
124
  return True
97
-
125
+
126
+ fixes = _sort_fixes_by_priority(fixes)
98
127
  io.print_executing_all(len(fixes))
99
128
  summary_lines = []
100
129
  for cmd, comment in fixes:
@@ -192,12 +221,24 @@ def handle_free_text(user_in: str, messages: list) -> bool:
192
221
  def run_single_command(cmd: str, comment: str) -> CmdResult:
193
222
  """Run a command with full transparency and safety checks."""
194
223
  cmd = elevate_cmd(cmd)
224
+
225
+ # Check for dangerous commands
195
226
  danger = is_dangerous(cmd)
196
227
  if danger:
197
228
  io.print_blocked_command(cmd, danger)
198
229
  return CmdResult(cmd=cmd, comment=comment, ok=False,
199
230
  stdout="", stderr=f"Zablokowano: {danger}", returncode=-99)
200
231
 
232
+ # Check for interactive blockers
233
+ blocker = is_interactive_blocker(cmd)
234
+ if blocker:
235
+ from rich.text import Text
236
+ io.console.print(f"\n [bold yellow]⚠️ OSTRZEŻENIE:[/bold yellow] {blocker}")
237
+ io.console.print(f" Ta komenda może zawiesić sesję w trybie nieinteraktywnym.")
238
+ if io.console.input(" Czy na pewno chcesz spróbować? [y/N]: ").lower() not in ("y", "yes", "tak"):
239
+ return CmdResult(cmd=cmd, comment=comment, ok=False,
240
+ stdout="", stderr="Anulowano przez użytkownika (interaktywna).", returncode=-1, skipped=True)
241
+
201
242
  io.print_cmd_preview(cmd, comment)
202
243
  ans = io.ask_execute_prompt()
203
244
  if ans in ("n", "no", "nie"):
@@ -206,7 +247,11 @@ def run_single_command(cmd: str, comment: str) -> CmdResult:
206
247
 
207
248
  timeout = _resolve_command_timeout(cmd)
208
249
  io.console.print(" [dim]⏳ Wykonuję...[/dim]", end="")
209
- ok, stdout, stderr, rc = run_command(cmd, timeout=timeout)
250
+
251
+ # Suspend session timeout during command execution
252
+ with io.suspend_timeout():
253
+ ok, stdout, stderr, rc = run_command(cmd, timeout=timeout)
254
+
210
255
  io.console.print("\r" + " " * 30 + "\r", end="")
211
256
  result = CmdResult(cmd=cmd, comment=comment, ok=ok,
212
257
  stdout=stdout, stderr=stderr, returncode=rc)
@@ -30,7 +30,7 @@ _session_ref = None
30
30
 
31
31
 
32
32
  @contextmanager
33
- def _suspend_timeout():
33
+ def suspend_timeout():
34
34
  """Context manager to temporarily suspend session timeout during user input."""
35
35
  global _timeout_handler, _timeout_seconds, _session_ref
36
36
  try:
@@ -118,7 +118,7 @@ def ask_user_problem() -> str:
118
118
  console.print()
119
119
  console.print(Panel(body, title="[bold cyan]💬 OPISZ SWÓJ PROBLEM[/bold cyan]", border_style="cyan"))
120
120
  try:
121
- with _suspend_timeout():
121
+ with suspend_timeout():
122
122
  return console.input(" [bold cyan]Twój problem:[/bold cyan] ").strip()
123
123
  except (EOFError, KeyboardInterrupt):
124
124
  return ""
@@ -230,13 +230,13 @@ def print_searching() -> None:
230
230
 
231
231
  def ask_execute_prompt() -> str:
232
232
  """Ask user if they want to execute a command."""
233
- with _suspend_timeout():
233
+ with suspend_timeout():
234
234
  return console.input(" [bold]Wykonać?[/bold] \\[Y/n]: ").strip().lower()
235
235
 
236
236
 
237
237
  def ask_low_confidence_search() -> bool:
238
238
  """Ask user if they want to search when LLM is uncertain."""
239
- with _suspend_timeout():
239
+ with suspend_timeout():
240
240
  return console.input(
241
241
  "\n [dim]💡 LLM niepewny – szukać zewnętrznie? [y/N]:[/dim] "
242
242
  ).strip().lower() in ("y", "yes", "tak")
@@ -244,7 +244,7 @@ def ask_low_confidence_search() -> bool:
244
244
 
245
245
  def ask_send_data() -> bool:
246
246
  """Ask user if they want to send data to LLM."""
247
- with _suspend_timeout():
247
+ with suspend_timeout():
248
248
  ans = console.input("\n Czy wysłać te dane do LLM? \\[Y/n]: ").strip().lower()
249
249
  return ans not in ("n", "no", "nie")
250
250
 
@@ -252,7 +252,7 @@ def ask_send_data() -> bool:
252
252
  def get_user_input(remaining: int) -> str:
253
253
  """Get user input with prompt."""
254
254
  try:
255
- with _suspend_timeout():
255
+ with suspend_timeout():
256
256
  return console.input(f"\n [bold cyan]fixos [{fmt_time(remaining)}] ❯[/bold cyan] ").strip()
257
257
  except (EOFError, KeyboardInterrupt):
258
258
  return ""
@@ -36,7 +36,7 @@ _OBJECT_KEYWORDS: list[tuple[list[str], tuple]] = [
36
36
  ]
37
37
 
38
38
 
39
- def _object_based_match(prompt_lower: str) -> object:
39
+ def _object_based_match(prompt_lower: str) -> object | None:
40
40
  """Fallback object-based matching when no action keyword is found."""
41
41
  for keywords, cmd in _OBJECT_KEYWORDS:
42
42
  if any(kw in prompt_lower for kw in keywords):
@@ -44,7 +44,7 @@ def _object_based_match(prompt_lower: str) -> object:
44
44
  return None
45
45
 
46
46
 
47
- def _match_heuristic_command(prompt_lower: str) -> object:
47
+ def _match_heuristic_command(prompt_lower: str) -> object | None:
48
48
  """
49
49
  Match user prompt against heuristic keyword mappings.
50
50
 
@@ -62,14 +62,15 @@ def _match_heuristic_command(prompt_lower: str) -> object:
62
62
  return _object_based_match(prompt_lower)
63
63
 
64
64
 
65
- def _format_command(matched_cmd) -> str:
65
+ def _format_command(matched_cmd: object) -> str:
66
66
  """Convert matched command to string format."""
67
67
  if isinstance(matched_cmd, str):
68
68
  return matched_cmd
69
- else:
69
+ elif isinstance(matched_cmd, (list, tuple)):
70
70
  cmd_program = matched_cmd[0]
71
71
  cmd_args = matched_cmd[1] if len(matched_cmd) > 1 else []
72
- return " ".join([cmd_program] + cmd_args)
72
+ return " ".join([str(cmd_program)] + [str(a) for a in cmd_args])
73
+ return str(matched_cmd)
73
74
 
74
75
 
75
76
  def _build_output_dict(
@@ -4,7 +4,16 @@ Cleanup command for fixOS CLI - service data cleanup with detailed flatpak suppo
4
4
  import click
5
5
  import subprocess
6
6
  from fixos.diagnostics.service_scanner import ServiceDataScanner
7
-
7
+ from fixos.constants import (
8
+ DEFAULT_COMMAND_TIMEOUT,
9
+ FAST_COMMAND_TIMEOUT,
10
+ MAX_SEARCH_QUERY_LENGTH,
11
+ HOSTNAME_DISPLAY_LENGTH,
12
+ CONFIG_DISPLAY_LENGTH,
13
+ MAX_OUTPUT_LINES,
14
+ )
15
+
16
+ # Local constants for internal logic
8
17
  CONSTANT_3 = 3
9
18
  CONSTANT_4 = 4
10
19
  CONSTANT_5 = 5
@@ -16,7 +25,7 @@ CONSTANT_50 = 50
16
25
  CONSTANT_60 = 60
17
26
  CONSTANT_90 = 90
18
27
  CONSTANT_120 = 120
19
- CONSTANT_300 = 300
28
+ CONSTANT_300 = DEFAULT_COMMAND_TIMEOUT
20
29
  CONSTANT_500 = 500
21
30
  CONSTANT_1024 = 1024
22
31
 
@@ -104,7 +113,7 @@ def cleanup_services(threshold, services, json_output, cleanup, dry_run, list_on
104
113
  def _display_cleanup_summary(plan: dict, threshold: int) -> None:
105
114
  """Display cleanup plan summary header."""
106
115
  click.echo(click.style(f"\nSkanowanie usług (próg: {threshold} MB)...", fg="cyan"))
107
- click.echo(click.style("═" * CONSTANT_60, fg="cyan"))
116
+ click.echo(click.style(f"{'' * CONSTANT_60}", fg="cyan"))
108
117
 
109
118
  if plan["services_found"] == 0:
110
119
  click.echo(click.style("\nNie znaleziono usług powyżej progu.", fg="green"))
@@ -281,9 +290,9 @@ def _cleanup_flatpak_detailed(scanner, json_output: bool, dry_run: bool) -> None
281
290
  return
282
291
 
283
292
  # Wyświetl menu z opcjami
284
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
293
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
285
294
  click.echo(click.style("📋 WYBIERZ OPCJE DO WYKONANIA", fg="cyan", bold=True))
286
- click.echo(click.style("="*CONSTANT_60, fg="cyan"))
295
+ click.echo(click.style(f"{'='*CONSTANT_60}", fg="cyan"))
287
296
 
288
297
  if dry_run:
289
298
  click.echo(click.style("\n[TRYB DRY-RUN] - brak faktycznych zmian\n", fg="yellow"))
@@ -309,9 +318,9 @@ def _cleanup_flatpak_detailed(scanner, json_output: bool, dry_run: bool) -> None
309
318
  click.echo(f" {click.style(f'Elementów: {len(rec["items"])}', fg='white', dim=True)}")
310
319
 
311
320
  # Podsumowanie potencjalnych korzyści
312
- click.echo("\n" + click.style("-"*CONSTANT_60, fg="cyan"))
321
+ click.echo(f"\n{click.style('-'*CONSTANT_60, fg='cyan')}")
313
322
  click.echo(f"💰 {click.style('ŁĄCZNA POTENCJALNA KORZYŚĆ:', fg='green', bold=True)} ~{_format_bytes(total_potential_savings)}")
314
- click.echo(click.style("-"*CONSTANT_60, fg="cyan"))
323
+ click.echo(click.style(f"{'-'*CONSTANT_60}", fg="cyan"))
315
324
 
316
325
  # Menu wyboru
317
326
  click.echo(f"\n{click.style('Dostępne opcje:', fg='white', bold=True)}")
@@ -353,9 +362,9 @@ def _cleanup_flatpak_detailed(scanner, json_output: bool, dry_run: bool) -> None
353
362
  "space_reclaimed": 0,
354
363
  }
355
364
 
356
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
365
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
357
366
  click.echo(click.style("🚀 WYKONYWANIE WYBRANYCH AKCJI", fg="cyan", bold=True))
358
- click.echo(click.style("="*CONSTANT_60, fg="cyan") + "\n")
367
+ click.echo(f"{click.style('='*CONSTANT_60, fg='cyan')}\n")
359
368
 
360
369
  for idx in selected_indices:
361
370
  rec = recommendations[idx]
@@ -385,9 +394,9 @@ def _cleanup_flatpak_detailed(scanner, json_output: bool, dry_run: bool) -> None
385
394
  click.echo(click.style(f" ❌ Błąd: {result.get('error', 'Unknown error')}", fg="red"))
386
395
 
387
396
  # Podsumowanie końcowe
388
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
397
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
389
398
  click.echo(click.style("📊 PODSUMOWANIE", fg="cyan", bold=True))
390
- click.echo(click.style("="*CONSTANT_60, fg="cyan"))
399
+ click.echo(click.style(f"{'='*CONSTANT_60}", fg="cyan"))
391
400
  click.echo(f" ✅ Wykonano: {len(results['executed'])}")
392
401
  click.echo(f" ⏭️ Pominięto: {len(results['skipped'])}")
393
402
  click.echo(f" ❌ Błędy: {len(results['failed'])}")
@@ -403,9 +412,9 @@ def _cleanup_flatpak_detailed(scanner, json_output: bool, dry_run: bool) -> None
403
412
 
404
413
  def _display_flatpak_status(analysis: dict) -> None:
405
414
  """Wyświetl status Flatpak z rzeczywistymi danymi"""
406
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
415
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
407
416
  click.echo(click.style("📊 STATUS FLATPAK", fg="cyan", bold=True))
408
- click.echo(click.style("="*CONSTANT_60, fg="cyan"))
417
+ click.echo(click.style(f"{'='*CONSTANT_60}", fg="cyan"))
409
418
 
410
419
  # Aplikacje
411
420
  apps_count = len(analysis.get('installed_apps', []))
@@ -449,13 +458,13 @@ def _display_flatpak_status(analysis: dict) -> None:
449
458
 
450
459
  def _display_detailed_recommendations(recommendations: list) -> None:
451
460
  """Wyświetl szczegółowe informacje o każdej rekomendacji"""
452
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
461
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
453
462
  click.echo(click.style("📖 SZCZEGÓŁY REKOMENDACJI", fg="cyan", bold=True))
454
- click.echo(click.style("="*CONSTANT_60, fg="cyan"))
463
+ click.echo(click.style(f"{'='*CONSTANT_60}", fg="cyan"))
455
464
 
456
465
  for i, rec in enumerate(recommendations, 1):
457
466
  click.echo(f"\n{click.style(f'[{i}]', fg='cyan', bold=True)} {rec['description']}")
458
- click.echo(click.style("-"*CONSTANT_50, fg="white", dim=True))
467
+ click.echo(click.style(f"{'-'*CONSTANT_50}", fg="white", dim=True))
459
468
  click.echo(f"\n{rec['explanation']}")
460
469
 
461
470
  if rec.get('items'):
@@ -544,9 +553,9 @@ def _build_dep_types(items: list) -> dict:
544
553
 
545
554
  def _display_full_system_menu(analyzer, analysis: dict, safe_items: list, medium_items: list, dry_run: bool) -> str:
546
555
  """Display recommendations and menu for full system cleanup. Returns user selection."""
547
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
556
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
548
557
  click.echo(click.style("📋 REKOMENDACJE", fg="cyan", bold=True))
549
- click.echo(click.style("="*CONSTANT_60, fg="cyan"))
558
+ click.echo(click.style(f"{'='*CONSTANT_60}", fg="cyan"))
550
559
 
551
560
  if dry_run:
552
561
  click.echo(click.style("\n[TRYB DRY-RUN] - brak faktycznych zmian\n", fg="yellow"))
@@ -567,9 +576,9 @@ def _display_full_system_menu(analyzer, analysis: dict, safe_items: list, medium
567
576
  click.echo(f" → {click.style(item.cleanup_command, fg='cyan', dim=True)}")
568
577
  click.echo(f"\n 💰 Łącznie: {click.style(_format_bytes(total_medium), fg='yellow')}")
569
578
 
570
- click.echo("\n" + click.style("-"*CONSTANT_60, fg="cyan"))
579
+ click.echo(f"\n{click.style('-'*CONSTANT_60, fg='cyan')}")
571
580
  click.echo(f"💰 {click.style('ŁĄCZNIE DO ODZYSKANIA:', fg='green', bold=True)} {analysis['total_reclaimable_human']}")
572
- click.echo(click.style("-"*CONSTANT_60, fg="cyan"))
581
+ click.echo(click.style(f"{'-'*CONSTANT_60}", fg="cyan"))
573
582
 
574
583
  dev_items = [item for item in analyzer.items if item.category == 'dev_projects']
575
584
  if dev_items:
@@ -1094,9 +1103,9 @@ def _select_cleanup_items_by_filter(selection: str, analyzer, safe_items: list)
1094
1103
 
1095
1104
  def _execute_full_cleanup(items_to_clean: list, dry_run: bool) -> None:
1096
1105
  """Execute cleanup commands for a list of StorageItems."""
1097
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
1106
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
1098
1107
  click.echo(click.style("🚀 WYKONYWANIE CZYSZCZENIA", fg="cyan", bold=True))
1099
- click.echo(click.style("="*CONSTANT_60, fg="cyan") + "\n")
1108
+ click.echo(f"{click.style('='*CONSTANT_60, fg='cyan')}\n")
1100
1109
 
1101
1110
  results = {"success": 0, "failed": 0, "space_reclaimed": 0}
1102
1111
  for item in items_to_clean:
@@ -1127,9 +1136,9 @@ def _execute_full_cleanup(items_to_clean: list, dry_run: bool) -> None:
1127
1136
  click.echo(click.style(f" ❌ Błąd: {e}", fg="red"))
1128
1137
  results['failed'] += 1
1129
1138
 
1130
- click.echo("\n" + click.style("="*CONSTANT_60, fg="cyan"))
1139
+ click.echo(f"\n{click.style('='*CONSTANT_60, fg='cyan')}")
1131
1140
  click.echo(click.style("📊 PODSUMOWANIE", fg="cyan", bold=True))
1132
- click.echo(click.style("="*CONSTANT_60, fg="cyan"))
1141
+ click.echo(click.style(f"{'='*CONSTANT_60}", fg="cyan"))
1133
1142
  click.echo(f" ✅ Sukces: {results['success']}")
1134
1143
  click.echo(f" ❌ Błędy: {results['failed']}")
1135
1144
  if dry_run:
@@ -88,6 +88,24 @@ def is_dangerous(cmd: str) -> Optional[str]:
88
88
  return None
89
89
 
90
90
 
91
+ def is_interactive_blocker(cmd: str) -> Optional[str]:
92
+ """Returns reason string if command is likely to hang in non-interactive session."""
93
+ import re
94
+ patterns = [
95
+ (r"\bnewgrp\b", "newgrp replaces the shell and waits for input"),
96
+ (r"\bsu\s+-(\s+|$)", "su - starts a new login shell"),
97
+ (r"\bexec\s+bash\b", "exec replaces the process"),
98
+ (r"\btop\b(?!.*\b-b\b)", "top is interactive unless run in batch mode (-b)"),
99
+ (r"\bvim?\b", "editors require terminal interaction"),
100
+ (r"\bnano\b", "editors require terminal interaction"),
101
+ (r"\bless\b", "pagers require terminal interaction"),
102
+ ]
103
+ for pat, reason in patterns:
104
+ if re.search(pat, cmd, re.IGNORECASE):
105
+ return reason
106
+ return None
107
+
108
+
91
109
  def run_command(
92
110
  cmd: str,
93
111
  timeout: int = 120,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.20
3
+ Version: 2.2.22
4
4
  Summary: AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues
5
5
  Home-page: https://github.com/wronai/fixos
6
6
  Author: fixos contributors
@@ -63,11 +63,11 @@ AI-powered OS Diagnostics
63
63
 
64
64
  ## AI Cost Tracking
65
65
 
66
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.20-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-22.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
66
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-22.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
68
68
 
69
- - 🤖 **LLM usage:** $7.5000 (119 commits)
70
- - 👤 **Human dev:** ~$2223 (22.2h @ $100/h, 30min dedup)
69
+ - 🤖 **LLM usage:** $7.5000 (121 commits)
70
+ - 👤 **Human dev:** ~$2280 (22.8h @ $100/h, 30min dedup)
71
71
 
72
72
  Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
73
73
 
@@ -4,6 +4,7 @@ dist
4
4
  docs
5
5
  fixos
6
6
  project
7
+ scratch
7
8
  scripts
8
9
  testql-scenarios
9
10
  venv
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixos"
7
- version = "2.2.20"
7
+ version = "2.2.22"
8
8
  description = "AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -5,7 +5,7 @@ long_description = (Path(__file__).parent / "README.md").read_text(encoding="utf
5
5
 
6
6
  setup(
7
7
  name="fixos",
8
- version="2.2.19",
8
+ version="2.2.21",
9
9
  description="AI-powered Linux/Windows diagnostics and repair with anonymization",
10
10
  long_description=long_description,
11
11
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes