arch-ops-server 3.3.1__tar.gz → 3.3.3__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 (18) hide show
  1. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/PKG-INFO +1 -1
  2. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/pyproject.toml +1 -1
  3. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/__init__.py +9 -1
  4. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/pacman.py +254 -0
  5. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/server.py +67 -184
  6. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/system_health_check.py +14 -13
  7. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/README.md +0 -0
  8. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/aur.py +0 -0
  9. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/config.py +0 -0
  10. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/http_server.py +0 -0
  11. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/logs.py +0 -0
  12. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/mirrors.py +0 -0
  13. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/news.py +0 -0
  14. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/py.typed +0 -0
  15. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/system.py +0 -0
  16. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/tool_metadata.py +0 -0
  17. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/utils.py +0 -0
  18. {arch_ops_server-3.3.1 → arch_ops_server-3.3.3}/src/arch_ops_server/wiki.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arch-ops-server
3
- Version: 3.3.1
3
+ Version: 3.3.3
4
4
  Summary: MCP server bridging AI assistants with Arch Linux ecosystem (Wiki, AUR, official repos)
5
5
  Keywords: arch-linux,mcp,model-context-protocol,aur,pacman,wiki,ai-assistant
6
6
  Author: Nihal
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arch-ops-server"
3
- version = "3.3.1"
3
+ version = "3.3.3"
4
4
  description = "MCP server bridging AI assistants with Arch Linux ecosystem (Wiki, AUR, official repos)"
5
5
  readme = {file = "README.md", content-type = "text/markdown"}
6
6
  license = { text = "GPL-3.0-only OR MIT" }
@@ -6,7 +6,7 @@ A Model Context Protocol server that bridges AI assistants with the Arch Linux
6
6
  ecosystem, providing access to the Arch Wiki, AUR, and official repositories.
7
7
  """
8
8
 
9
- __version__ = "3.3.1"
9
+ __version__ = "3.3.3"
10
10
 
11
11
  from .wiki import search_wiki, get_wiki_page, get_wiki_page_as_text
12
12
  from .aur import (
@@ -23,17 +23,21 @@ from .pacman import (
23
23
  check_updates_dry_run,
24
24
  remove_package,
25
25
  remove_packages_batch,
26
+ remove_packages,
26
27
  list_orphan_packages,
27
28
  remove_orphans,
29
+ manage_orphans,
28
30
  find_package_owner,
29
31
  list_package_files,
30
32
  search_package_files,
33
+ query_file_ownership,
31
34
  verify_package_integrity,
32
35
  list_package_groups,
33
36
  list_group_packages,
34
37
  list_explicit_packages,
35
38
  mark_as_explicit,
36
39
  mark_as_dependency,
40
+ manage_install_reason,
37
41
  check_database_freshness
38
42
  )
39
43
  from .system import (
@@ -130,17 +134,21 @@ __all__ = [
130
134
  "check_updates_dry_run",
131
135
  "remove_package",
132
136
  "remove_packages_batch",
137
+ "remove_packages",
133
138
  "list_orphan_packages",
134
139
  "remove_orphans",
140
+ "manage_orphans",
135
141
  "find_package_owner",
136
142
  "list_package_files",
137
143
  "search_package_files",
144
+ "query_file_ownership",
138
145
  "verify_package_integrity",
139
146
  "list_package_groups",
140
147
  "list_group_packages",
141
148
  "list_explicit_packages",
142
149
  "mark_as_explicit",
143
150
  "mark_as_dependency",
151
+ "manage_install_reason",
144
152
  "check_database_freshness",
145
153
  # System
146
154
  "get_system_info",
@@ -472,6 +472,70 @@ async def remove_packages_batch(
472
472
  )
473
473
 
474
474
 
475
+ async def remove_packages(
476
+ packages: Union[str, List[str]],
477
+ remove_dependencies: bool = False,
478
+ force: bool = False
479
+ ) -> Dict[str, Any]:
480
+ """
481
+ Unified tool for removing packages (single or multiple).
482
+
483
+ This consolidates two operations:
484
+ - Single package removal (replaces remove_package)
485
+ - Batch package removal (replaces remove_packages_batch)
486
+
487
+ Args:
488
+ packages: Package name (string) or list of package names to remove
489
+ remove_dependencies: If True, remove unneeded dependencies (pacman -Rs)
490
+ force: If True, force removal ignoring dependencies (pacman -Rdd). Only works for single package.
491
+
492
+ Returns:
493
+ Dict with removal status and information
494
+ """
495
+ if not IS_ARCH:
496
+ return create_error_response(
497
+ "NotSupported",
498
+ "Package removal is only available on Arch Linux"
499
+ )
500
+
501
+ if not check_command_exists("pacman"):
502
+ return create_error_response(
503
+ "CommandNotFound",
504
+ "pacman command not found"
505
+ )
506
+
507
+ # Normalize input to list
508
+ if isinstance(packages, str):
509
+ package_list = [packages]
510
+ is_single = True
511
+ else:
512
+ package_list = packages
513
+ is_single = False
514
+
515
+ if not package_list:
516
+ return create_error_response(
517
+ "ValidationError",
518
+ "No packages specified for removal"
519
+ )
520
+
521
+ # Validate force flag usage
522
+ if force and not is_single:
523
+ return create_error_response(
524
+ "ValidationError",
525
+ "force flag can only be used with single package removal"
526
+ )
527
+
528
+ logger.info(f"Removing {len(package_list)} package(s): {package_list} (deps={remove_dependencies}, force={force})")
529
+
530
+ # Route to appropriate implementation based on input type and flags
531
+ if is_single:
532
+ # Single package removal
533
+ return await remove_package(package_list[0], remove_dependencies, force)
534
+ else:
535
+ # Batch package removal (force not supported)
536
+ return await remove_packages_batch(package_list, remove_dependencies)
537
+
538
+
475
539
  async def list_orphan_packages() -> Dict[str, Any]:
476
540
  """
477
541
  List all orphaned packages (dependencies no longer required).
@@ -628,6 +692,64 @@ async def remove_orphans(dry_run: bool = True, exclude: Optional[List[str]] = No
628
692
  )
629
693
 
630
694
 
695
+ async def manage_orphans(
696
+ action: str,
697
+ dry_run: bool = True,
698
+ exclude: Optional[List[str]] = None
699
+ ) -> Dict[str, Any]:
700
+ """
701
+ Unified tool for managing orphaned packages.
702
+
703
+ This consolidates two operations:
704
+ - list: List all orphaned packages (replaces list_orphan_packages)
705
+ - remove: Remove orphaned packages (replaces remove_orphans)
706
+
707
+ Args:
708
+ action: Action to perform - "list" or "remove"
709
+ dry_run: If True (default), show what would be removed without removing (only for remove action)
710
+ exclude: List of packages to exclude from removal (only for remove action)
711
+
712
+ Returns:
713
+ Dict with action results
714
+ """
715
+ if not IS_ARCH:
716
+ return create_error_response(
717
+ "NotSupported",
718
+ "Orphan package management is only available on Arch Linux"
719
+ )
720
+
721
+ if not check_command_exists("pacman"):
722
+ return create_error_response(
723
+ "CommandNotFound",
724
+ "pacman command not found"
725
+ )
726
+
727
+ # Validate action
728
+ valid_actions = ["list", "remove"]
729
+ if action not in valid_actions:
730
+ return create_error_response(
731
+ "ValidationError",
732
+ f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"
733
+ )
734
+
735
+ logger.info(f"Orphan management: action={action}, dry_run={dry_run}")
736
+
737
+ # Route to appropriate implementation based on action
738
+ if action == "list":
739
+ # List orphaned packages
740
+ return await list_orphan_packages()
741
+
742
+ elif action == "remove":
743
+ # Remove orphaned packages
744
+ return await remove_orphans(dry_run, exclude)
745
+
746
+ # This should never be reached due to validation above
747
+ return create_error_response(
748
+ "InternalError",
749
+ f"Unexpected action: {action}"
750
+ )
751
+
752
+
631
753
  async def find_package_owner(file_path: str) -> Dict[str, Any]:
632
754
  """
633
755
  Find which package owns a specific file.
@@ -861,6 +983,69 @@ async def search_package_files(filename_pattern: str) -> Dict[str, Any]:
861
983
  )
862
984
 
863
985
 
986
+ async def query_file_ownership(
987
+ query: str,
988
+ mode: str,
989
+ filter_pattern: Optional[str] = None
990
+ ) -> Dict[str, Any]:
991
+ """
992
+ Unified tool for querying file ownership relationships.
993
+
994
+ This consolidates three operations:
995
+ - file_to_package: Find which package owns a specific file (replaces find_package_owner)
996
+ - package_to_files: List all files owned by a package (replaces list_package_files)
997
+ - filename_search: Search for files across all packages (replaces search_package_files)
998
+
999
+ Args:
1000
+ query: The query string (file path, package name, or filename pattern depending on mode)
1001
+ mode: Query mode - "file_to_package", "package_to_files", or "filename_search"
1002
+ filter_pattern: Optional regex pattern to filter files (only used in package_to_files mode)
1003
+
1004
+ Returns:
1005
+ Dict with query results appropriate to the mode
1006
+ """
1007
+ if not IS_ARCH:
1008
+ return create_error_response(
1009
+ "NotSupported",
1010
+ "File ownership queries are only available on Arch Linux"
1011
+ )
1012
+
1013
+ if not check_command_exists("pacman"):
1014
+ return create_error_response(
1015
+ "CommandNotFound",
1016
+ "pacman command not found"
1017
+ )
1018
+
1019
+ # Validate mode
1020
+ valid_modes = ["file_to_package", "package_to_files", "filename_search"]
1021
+ if mode not in valid_modes:
1022
+ return create_error_response(
1023
+ "ValidationError",
1024
+ f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)}"
1025
+ )
1026
+
1027
+ logger.info(f"File ownership query: mode={mode}, query={query}")
1028
+
1029
+ # Route to appropriate implementation based on mode
1030
+ if mode == "file_to_package":
1031
+ # Find which package owns a specific file (replaces find_package_owner)
1032
+ return await find_package_owner(query)
1033
+
1034
+ elif mode == "package_to_files":
1035
+ # List all files owned by a package (replaces list_package_files)
1036
+ return await list_package_files(query, filter_pattern)
1037
+
1038
+ elif mode == "filename_search":
1039
+ # Search for files across all packages (replaces search_package_files)
1040
+ return await search_package_files(query)
1041
+
1042
+ # This should never be reached due to validation above
1043
+ return create_error_response(
1044
+ "InternalError",
1045
+ f"Unexpected mode: {mode}"
1046
+ )
1047
+
1048
+
864
1049
  async def verify_package_integrity(package_name: str, thorough: bool = False) -> Dict[str, Any]:
865
1050
  """
866
1051
  Verify integrity of an installed package.
@@ -1213,6 +1398,75 @@ async def mark_as_dependency(package_name: str) -> Dict[str, Any]:
1213
1398
  )
1214
1399
 
1215
1400
 
1401
+ async def manage_install_reason(
1402
+ action: str,
1403
+ package_name: Optional[str] = None
1404
+ ) -> Dict[str, Any]:
1405
+ """
1406
+ Unified tool for managing package install reasons.
1407
+
1408
+ This consolidates three operations:
1409
+ - list: List all explicitly installed packages (replaces list_explicit_packages)
1410
+ - mark_explicit: Mark a package as explicitly installed (replaces mark_as_explicit)
1411
+ - mark_dependency: Mark a package as a dependency (replaces mark_as_dependency)
1412
+
1413
+ Args:
1414
+ action: Action to perform - "list", "mark_explicit", or "mark_dependency"
1415
+ package_name: Package name (required for mark_explicit and mark_dependency actions)
1416
+
1417
+ Returns:
1418
+ Dict with operation results appropriate to the action
1419
+ """
1420
+ if not IS_ARCH:
1421
+ return create_error_response(
1422
+ "NotSupported",
1423
+ "Install reason management is only available on Arch Linux"
1424
+ )
1425
+
1426
+ if not check_command_exists("pacman"):
1427
+ return create_error_response(
1428
+ "CommandNotFound",
1429
+ "pacman command not found"
1430
+ )
1431
+
1432
+ # Validate action
1433
+ valid_actions = ["list", "mark_explicit", "mark_dependency"]
1434
+ if action not in valid_actions:
1435
+ return create_error_response(
1436
+ "ValidationError",
1437
+ f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"
1438
+ )
1439
+
1440
+ # Validate package_name for marking actions
1441
+ if action in ["mark_explicit", "mark_dependency"] and not package_name:
1442
+ return create_error_response(
1443
+ "ValidationError",
1444
+ f"package_name is required for action '{action}'"
1445
+ )
1446
+
1447
+ logger.info(f"Install reason management: action={action}, package={package_name}")
1448
+
1449
+ # Route to appropriate implementation based on action
1450
+ if action == "list":
1451
+ # List all explicitly installed packages
1452
+ return await list_explicit_packages()
1453
+
1454
+ elif action == "mark_explicit":
1455
+ # Mark package as explicitly installed
1456
+ return await mark_as_explicit(package_name)
1457
+
1458
+ elif action == "mark_dependency":
1459
+ # Mark package as dependency
1460
+ return await mark_as_dependency(package_name)
1461
+
1462
+ # This should never be reached due to validation above
1463
+ return create_error_response(
1464
+ "InternalError",
1465
+ f"Unexpected action: {action}"
1466
+ )
1467
+
1468
+
1469
+
1216
1470
  async def check_database_freshness() -> Dict[str, Any]:
1217
1471
  """
1218
1472
  Check when package databases were last synchronized.
@@ -40,17 +40,15 @@ from . import (
40
40
  check_updates_dry_run,
41
41
  remove_package,
42
42
  remove_packages_batch,
43
+ remove_packages,
43
44
  list_orphan_packages,
44
45
  remove_orphans,
45
- find_package_owner,
46
- list_package_files,
47
- search_package_files,
46
+ manage_orphans,
47
+ query_file_ownership,
48
48
  verify_package_integrity,
49
49
  list_package_groups,
50
50
  list_group_packages,
51
- list_explicit_packages,
52
- mark_as_explicit,
53
- mark_as_dependency,
51
+ manage_install_reason,
54
52
  check_database_freshness,
55
53
  # System functions
56
54
  get_system_info,
@@ -751,20 +749,23 @@ async def list_tools() -> list[Tool]:
751
749
  annotations=ToolAnnotations(readOnlyHint=True)
752
750
  ),
753
751
 
754
- # Package Removal Tools
752
+ # Package Removal
755
753
  Tool(
756
- name="remove_package",
757
- description="[LIFECYCLE] Remove a package from the system. Supports various removal strategies: basic removal, removal with dependencies, or forced removal. Only works on Arch Linux. Requires sudo access. Example: Remove 'firefox' with dependencies using remove_dependencies=true, or force removal with force=true (dangerous!).",
754
+ name="remove_packages",
755
+ description="[LIFECYCLE] Unified tool for removing packages (single or multiple). Accepts either a single package name or a list of packages. Supports removal with dependencies and forced removal. Only works on Arch Linux. Requires sudo access. Examples: packages='firefox', remove_dependencies=true → removes Firefox with its dependencies; packages=['pkg1', 'pkg2', 'pkg3'] → batch removal of multiple packages; packages='lib', force=true → force removal ignoring dependencies (dangerous!).",
758
756
  inputSchema={
759
757
  "type": "object",
760
758
  "properties": {
761
- "package_name": {
762
- "type": "string",
763
- "description": "Name of the package to remove"
759
+ "packages": {
760
+ "oneOf": [
761
+ {"type": "string"},
762
+ {"type": "array", "items": {"type": "string"}}
763
+ ],
764
+ "description": "Package name (string) or list of package names (array) to remove"
764
765
  },
765
766
  "remove_dependencies": {
766
767
  "type": "boolean",
767
- "description": "Remove package and its dependencies (pacman -Rs). Default: false",
768
+ "description": "Remove packages and their dependencies (pacman -Rs). Default: false",
768
769
  "default": False
769
770
  },
770
771
  "force": {
@@ -773,118 +774,65 @@ async def list_tools() -> list[Tool]:
773
774
  "default": False
774
775
  }
775
776
  },
776
- "required": ["package_name"]
777
- },
778
- annotations=ToolAnnotations(destructiveHint=True)
779
- ),
780
-
781
- Tool(
782
- name="remove_packages_batch",
783
- description="[LIFECYCLE] Remove multiple packages in a single transaction. More efficient than removing packages one by one. Only works on Arch Linux. Requires sudo access. Use case: Clean up multiple packages at once: ['package1', 'package2', 'package3'] with optional dependency removal.",
784
- inputSchema={
785
- "type": "object",
786
- "properties": {
787
- "package_names": {
788
- "type": "array",
789
- "items": {"type": "string"},
790
- "description": "List of package names to remove"
791
- },
792
- "remove_dependencies": {
793
- "type": "boolean",
794
- "description": "Remove packages and their dependencies. Default: false",
795
- "default": False
796
- }
797
- },
798
- "required": ["package_names"]
777
+ "required": ["packages"]
799
778
  },
800
779
  annotations=ToolAnnotations(destructiveHint=True)
801
780
  ),
802
781
 
803
782
  # Orphan Package Management
804
783
  Tool(
805
- name="list_orphan_packages",
806
- description="[MAINTENANCE] List all orphaned packages (dependencies no longer required by any installed package). Shows package names and total disk space usage. Only works on Arch Linux. When to use: After removing packages, find orphaned dependencies that are no longer needed.",
807
- inputSchema={
808
- "type": "object",
809
- "properties": {}
810
- },
811
- annotations=ToolAnnotations(readOnlyHint=True)
812
- ),
813
-
814
- Tool(
815
- name="remove_orphans",
816
- description="[MAINTENANCE] Remove all orphaned packages to free up disk space. Supports dry-run mode to preview changes and package exclusion. Only works on Arch Linux. Requires sudo access. Example: Use dry_run=true first to preview, then dry_run=false to actually remove. Exclude critical packages with exclude=['pkg1'].",
784
+ name="manage_orphans",
785
+ description="[MAINTENANCE] Unified tool for managing orphaned packages (dependencies no longer required). Supports two actions: 'list' (show orphaned packages) and 'remove' (remove orphaned packages). Only works on Arch Linux. Requires sudo access for removal. Examples: action='list' shows all orphaned packages with disk usage; action='remove', dry_run=true preview what would be removed; action='remove', dry_run=false, exclude=['pkg1'] → remove all orphans except 'pkg1'.",
817
786
  inputSchema={
818
787
  "type": "object",
819
788
  "properties": {
789
+ "action": {
790
+ "type": "string",
791
+ "enum": ["list", "remove"],
792
+ "description": "Action to perform: 'list' (list orphaned packages) or 'remove' (remove orphaned packages)"
793
+ },
820
794
  "dry_run": {
821
795
  "type": "boolean",
822
- "description": "Preview what would be removed without actually removing. Default: true",
796
+ "description": "Preview what would be removed without actually removing (only for remove action). Default: true",
823
797
  "default": True
824
798
  },
825
799
  "exclude": {
826
800
  "type": "array",
827
801
  "items": {"type": "string"},
828
- "description": "List of package names to exclude from removal"
802
+ "description": "List of package names to exclude from removal (only for remove action)"
829
803
  }
830
804
  },
831
- "required": []
805
+ "required": ["action"]
832
806
  },
833
- annotations=ToolAnnotations(destructiveHint=True)
807
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False) # Mixed: list is read-only, remove is destructive
834
808
  ),
835
809
 
836
- # Package Ownership Tools
810
+ # File Ownership Query (Consolidated)
837
811
  Tool(
838
- name="find_package_owner",
839
- description="[ORGANIZATION] Find which package owns a specific file on the system. Useful for troubleshooting and understanding file origins. Only works on Arch Linux. Example: '/usr/bin/python' → returns 'python' package that owns this file.",
812
+ name="query_file_ownership",
813
+ description="[ORGANIZATION] Unified tool for querying file-package ownership relationships. Supports three modes: 'file_to_package' (find which package owns a file), 'package_to_files' (list all files in a package with optional filtering), and 'filename_search' (search for files across all packages). Only works on Arch Linux. Examples: mode='file_to_package', query='/usr/bin/python' → returns 'python' package; mode='package_to_files', query='systemd', filter_pattern='*.service' → lists all systemd service files; mode='filename_search', query='*.desktop' → finds all packages with desktop entries.",
840
814
  inputSchema={
841
815
  "type": "object",
842
816
  "properties": {
843
- "file_path": {
817
+ "query": {
844
818
  "type": "string",
845
- "description": "Absolute path to the file (e.g., /usr/bin/vim)"
846
- }
847
- },
848
- "required": ["file_path"]
849
- },
850
- annotations=ToolAnnotations(readOnlyHint=True)
851
- ),
852
-
853
- Tool(
854
- name="list_package_files",
855
- description="[ORGANIZATION] List all files owned by a package. Supports optional filtering by pattern. Only works on Arch Linux. Use case: See all files installed by 'systemd' package, optionally filter with pattern='*.service'.",
856
- inputSchema={
857
- "type": "object",
858
- "properties": {
859
- "package_name": {
819
+ "description": "Query string: file path for file_to_package mode, package name for package_to_files mode, or filename pattern for filename_search mode"
820
+ },
821
+ "mode": {
860
822
  "type": "string",
861
- "description": "Name of the package"
823
+ "enum": ["file_to_package", "package_to_files", "filename_search"],
824
+ "description": "Query mode: 'file_to_package' (find package owner), 'package_to_files' (list package files), or 'filename_search' (search across packages)"
862
825
  },
863
826
  "filter_pattern": {
864
827
  "type": "string",
865
- "description": "Optional regex pattern to filter files (e.g., '*.conf' or '/etc/')"
828
+ "description": "Optional regex pattern to filter files (only used in package_to_files mode, e.g., '*.conf' or '/etc/')"
866
829
  }
867
830
  },
868
- "required": ["package_name"]
831
+ "required": ["query", "mode"]
869
832
  },
870
833
  annotations=ToolAnnotations(readOnlyHint=True)
871
834
  ),
872
835
 
873
- Tool(
874
- name="search_package_files",
875
- description="[ORGANIZATION] Search for files across all packages in repositories. Requires package database sync (pacman -Fy). Only works on Arch Linux. Example: Search for '*.desktop' to find all packages that install desktop entries.",
876
- inputSchema={
877
- "type": "object",
878
- "properties": {
879
- "filename_pattern": {
880
- "type": "string",
881
- "description": "File name or pattern to search for (e.g., 'vim' or '*.service')"
882
- }
883
- },
884
- "required": ["filename_pattern"]
885
- },
886
- annotations=ToolAnnotations(readOnlyHint=True)
887
- ),
888
836
 
889
837
  # Package Verification
890
838
  Tool(
@@ -937,45 +885,24 @@ async def list_tools() -> list[Tool]:
937
885
 
938
886
  # Install Reason Management
939
887
  Tool(
940
- name="list_explicit_packages",
941
- description="[MAINTENANCE] List all packages explicitly installed by the user (not installed as dependencies). Useful for creating backup lists or understanding system composition. Only works on Arch Linux. When to use: Find packages you explicitly installed (vs dependencies) for system documentation.",
942
- inputSchema={
943
- "type": "object",
944
- "properties": {}
945
- },
946
- annotations=ToolAnnotations(readOnlyHint=True)
947
- ),
948
-
949
- Tool(
950
- name="mark_as_explicit",
951
- description="[MAINTENANCE] Mark a package as explicitly installed. Prevents it from being removed as an orphan. Only works on Arch Linux. Example: Mark 'python-pip' as explicit if you want to keep it even when dependencies change.",
888
+ name="manage_install_reason",
889
+ description="[MAINTENANCE] Unified tool for managing package install reasons. Supports three actions: 'list' (list all explicitly installed packages), 'mark_explicit' (prevent package from being removed as orphan), and 'mark_dependency' (allow package to be auto-removed with orphans). Only works on Arch Linux. Examples: action='list' returns all user-installed packages; action='mark_explicit', package_name='python-pip' keeps package even when dependencies change; action='mark_dependency', package_name='lib32-gcc-libs' → allows auto-removal with orphans.",
952
890
  inputSchema={
953
891
  "type": "object",
954
892
  "properties": {
955
- "package_name": {
893
+ "action": {
956
894
  "type": "string",
957
- "description": "Name of the package to mark as explicit"
958
- }
959
- },
960
- "required": ["package_name"]
961
- },
962
- annotations=ToolAnnotations(destructiveHint=True)
963
- ),
964
-
965
- Tool(
966
- name="mark_as_dependency",
967
- description="[MAINTENANCE] Mark a package as a dependency. Allows it to be removed as an orphan if no packages depend on it. Only works on Arch Linux. Use case: Mark 'lib32-gcc-libs' as dependency so it can be auto-removed with orphans later.",
968
- inputSchema={
969
- "type": "object",
970
- "properties": {
895
+ "enum": ["list", "mark_explicit", "mark_dependency"],
896
+ "description": "Action to perform: 'list' (list explicit packages), 'mark_explicit' (mark as user-installed), or 'mark_dependency' (mark as auto-removable)"
897
+ },
971
898
  "package_name": {
972
899
  "type": "string",
973
- "description": "Name of the package to mark as dependency"
900
+ "description": "Package name (required for mark_explicit and mark_dependency actions)"
974
901
  }
975
902
  },
976
- "required": ["package_name"]
903
+ "required": ["action"]
977
904
  },
978
- annotations=ToolAnnotations(destructiveHint=True)
905
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False) # Mixed: list is read-only, marking is destructive
979
906
  ),
980
907
 
981
908
  # System Diagnostic Tools
@@ -1334,66 +1261,36 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent |
1334
1261
  return [TextContent(type="text", text=json.dumps(result, indent=2))]
1335
1262
 
1336
1263
  # Package Removal Tools
1337
- elif name == "remove_package":
1264
+ elif name == "remove_packages":
1338
1265
  if not IS_ARCH:
1339
- return [TextContent(type="text", text=create_platform_error_message("remove_package"))]
1266
+ return [TextContent(type="text", text=create_platform_error_message("remove_packages"))]
1340
1267
 
1341
- package_name = arguments["package_name"]
1268
+ packages = arguments["packages"]
1342
1269
  remove_dependencies = arguments.get("remove_dependencies", False)
1343
1270
  force = arguments.get("force", False)
1344
- result = await remove_package(package_name, remove_dependencies, force)
1345
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
1346
-
1347
- elif name == "remove_packages_batch":
1348
- if not IS_ARCH:
1349
- return [TextContent(type="text", text=create_platform_error_message("remove_packages_batch"))]
1350
-
1351
- package_names = arguments["package_names"]
1352
- remove_dependencies = arguments.get("remove_dependencies", False)
1353
- result = await remove_packages_batch(package_names, remove_dependencies)
1271
+ result = await remove_packages(packages, remove_dependencies, force)
1354
1272
  return [TextContent(type="text", text=json.dumps(result, indent=2))]
1355
1273
 
1356
1274
  # Orphan Package Management
1357
- elif name == "list_orphan_packages":
1358
- if not IS_ARCH:
1359
- return [TextContent(type="text", text=create_platform_error_message("list_orphan_packages"))]
1360
-
1361
- result = await list_orphan_packages()
1362
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
1363
-
1364
- elif name == "remove_orphans":
1275
+ elif name == "manage_orphans":
1365
1276
  if not IS_ARCH:
1366
- return [TextContent(type="text", text=create_platform_error_message("remove_orphans"))]
1277
+ return [TextContent(type="text", text=create_platform_error_message("manage_orphans"))]
1367
1278
 
1279
+ action = arguments["action"]
1368
1280
  dry_run = arguments.get("dry_run", True)
1369
1281
  exclude = arguments.get("exclude", None)
1370
- result = await remove_orphans(dry_run, exclude)
1282
+ result = await manage_orphans(action, dry_run, exclude)
1371
1283
  return [TextContent(type="text", text=json.dumps(result, indent=2))]
1372
1284
 
1373
- # Package Ownership Tools
1374
- elif name == "find_package_owner":
1285
+ # File Ownership Query
1286
+ elif name == "query_file_ownership":
1375
1287
  if not IS_ARCH:
1376
- return [TextContent(type="text", text=create_platform_error_message("find_package_owner"))]
1288
+ return [TextContent(type="text", text=create_platform_error_message("query_file_ownership"))]
1377
1289
 
1378
- file_path = arguments["file_path"]
1379
- result = await find_package_owner(file_path)
1380
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
1381
-
1382
- elif name == "list_package_files":
1383
- if not IS_ARCH:
1384
- return [TextContent(type="text", text=create_platform_error_message("list_package_files"))]
1385
-
1386
- package_name = arguments["package_name"]
1290
+ query = arguments["query"]
1291
+ mode = arguments["mode"]
1387
1292
  filter_pattern = arguments.get("filter_pattern", None)
1388
- result = await list_package_files(package_name, filter_pattern)
1389
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
1390
-
1391
- elif name == "search_package_files":
1392
- if not IS_ARCH:
1393
- return [TextContent(type="text", text=create_platform_error_message("search_package_files"))]
1394
-
1395
- filename_pattern = arguments["filename_pattern"]
1396
- result = await search_package_files(filename_pattern)
1293
+ result = await query_file_ownership(query, mode, filter_pattern)
1397
1294
  return [TextContent(type="text", text=json.dumps(result, indent=2))]
1398
1295
 
1399
1296
  # Package Verification
@@ -1423,27 +1320,13 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent |
1423
1320
  return [TextContent(type="text", text=json.dumps(result, indent=2))]
1424
1321
 
1425
1322
  # Install Reason Management
1426
- elif name == "list_explicit_packages":
1427
- if not IS_ARCH:
1428
- return [TextContent(type="text", text=create_platform_error_message("list_explicit_packages"))]
1429
-
1430
- result = await list_explicit_packages()
1431
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
1432
-
1433
- elif name == "mark_as_explicit":
1323
+ elif name == "manage_install_reason":
1434
1324
  if not IS_ARCH:
1435
- return [TextContent(type="text", text=create_platform_error_message("mark_as_explicit"))]
1325
+ return [TextContent(type="text", text=create_platform_error_message("manage_install_reason"))]
1436
1326
 
1437
- package_name = arguments["package_name"]
1438
- result = await mark_as_explicit(package_name)
1439
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
1440
-
1441
- elif name == "mark_as_dependency":
1442
- if not IS_ARCH:
1443
- return [TextContent(type="text", text=create_platform_error_message("mark_as_dependency"))]
1444
-
1445
- package_name = arguments["package_name"]
1446
- result = await mark_as_dependency(package_name)
1327
+ action = arguments["action"]
1328
+ package_name = arguments.get("package_name", None)
1329
+ result = await manage_install_reason(action, package_name)
1447
1330
  return [TextContent(type="text", text=json.dumps(result, indent=2))]
1448
1331
 
1449
1332
  # System Diagnostic Tools
@@ -2107,7 +1990,7 @@ paru -S {package_name} # or yay -S {package_name}
2107
1990
  text=f"""Please perform a comprehensive system cleanup:
2108
1991
 
2109
1992
  1. **Check Orphaned Packages**:
2110
- - Run list_orphan_packages
1993
+ - Run manage_orphans with action='list'
2111
1994
  - Review the list for packages that can be safely removed
2112
1995
  {' - Be aggressive: remove all orphans unless critical' if aggressive else ' - Be conservative: keep packages that might be useful'}
2113
1996
 
@@ -2303,7 +2186,7 @@ Be detailed and provide specific mirror URLs and configuration commands."""
2303
2186
  - Identify any package operation failures
2304
2187
 
2305
2188
  5. **Package Integrity**:
2306
- - Run list_orphan_packages
2189
+ - Run manage_orphans with action='list'
2307
2190
  - Count orphaned packages and space used
2308
2191
  - Suggest running verify_package_integrity on critical packages
2309
2192
 
@@ -7,19 +7,6 @@ Provides a comprehensive system health check by integrating multiple system diag
7
7
  import logging
8
8
  from typing import Dict, Any
9
9
 
10
- from .utils import IS_ARCH
11
- from . import (
12
- get_system_info,
13
- check_disk_space,
14
- check_failed_services,
15
- get_pacman_cache_stats,
16
- check_updates_dry_run,
17
- check_critical_news,
18
- list_orphan_packages,
19
- check_database_freshness,
20
- check_mirrorlist_health
21
- )
22
-
23
10
  logger = logging.getLogger(__name__)
24
11
 
25
12
 
@@ -33,6 +20,20 @@ async def run_system_health_check() -> Dict[str, Any]:
33
20
  Returns:
34
21
  Dict with comprehensive health check results
35
22
  """
23
+ from .system import (
24
+ get_system_info,
25
+ check_disk_space,
26
+ check_failed_services,
27
+ get_pacman_cache_stats
28
+ )
29
+ from .pacman import (
30
+ check_updates_dry_run,
31
+ list_orphan_packages,
32
+ check_database_freshness
33
+ )
34
+ from .news import check_critical_news
35
+ from .mirrors import check_mirrorlist_health
36
+
36
37
  logger.info("Starting comprehensive system health check")
37
38
 
38
39
  health_report = {