rom-manager 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python
2
+ """rom-manager — ROM converter/organizer + MCP Server + A2A Agent.
3
+
4
+ Convert game ROMs to Compressed Hunks of Data (CHD) or RVZ, extract archives,
5
+ auto-rename via known game codes, and generate cue sheets. The core
6
+ :class:`RomManager` pipeline is wrapped by an MCP server and a Pydantic-AI agent.
7
+ """
8
+
9
+ from rom_manager.game_codes import psx_codes
10
+ from rom_manager.rom_manager import (
11
+ RomManager,
12
+ get_directory_size,
13
+ get_operating_system,
14
+ main,
15
+ rom_manager,
16
+ )
17
+ from rom_manager.version import __author__, __credits__, __version__
18
+
19
+ __all__ = [
20
+ "psx_codes",
21
+ "rom_manager",
22
+ "main",
23
+ "RomManager",
24
+ "get_operating_system",
25
+ "get_directory_size",
26
+ "__version__",
27
+ "__author__",
28
+ "__credits__",
29
+ ]
@@ -0,0 +1,4 @@
1
+ from rom_manager.agent_server import agent_server
2
+
3
+ if __name__ == "__main__":
4
+ agent_server()
@@ -0,0 +1,22 @@
1
+ """Adapter layer for ROM Manager.
2
+
3
+ CONCEPT:ROM-001, CONCEPT:ROM-002 — adapts the core pipeline for tool/agent
4
+ consumption. Houses the :class:`~rom_manager.adapters.api_client.Api` facade that
5
+ the MCP tools and the ``auth.get_client`` factory depend on.
6
+ """
7
+
8
+ from rom_manager.adapters.api_client import (
9
+ Api,
10
+ RomManager,
11
+ get_directory_size,
12
+ get_operating_system,
13
+ psx_codes,
14
+ )
15
+
16
+ __all__ = [
17
+ "Api",
18
+ "RomManager",
19
+ "psx_codes",
20
+ "get_operating_system",
21
+ "get_directory_size",
22
+ ]
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/python
2
+ """Local facade over the real ROM conversion pipeline.
3
+
4
+ ``rom-manager`` is not a REST API; this module is an **honest local facade**. It
5
+ re-exports the real callables from :mod:`rom_manager.rom_manager` and exposes a
6
+ thin :class:`Api` class that the MCP tools and the local ``auth.get_client``
7
+ factory depend on. Every method delegates to the genuine :class:`RomManager`
8
+ pipeline so behaviour is preserved exactly.
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ from typing import Any
14
+
15
+ from agent_utilities.base_utilities import get_logger
16
+
17
+ from rom_manager.game_codes import psx_codes
18
+ from rom_manager.rom_manager import (
19
+ RomManager,
20
+ get_directory_size,
21
+ get_operating_system,
22
+ )
23
+
24
+ logger = get_logger(__name__)
25
+
26
+ __all__ = [
27
+ "Api",
28
+ "RomManager",
29
+ "psx_codes",
30
+ "get_operating_system",
31
+ "get_directory_size",
32
+ ]
33
+
34
+
35
+ class Api:
36
+ """Thin facade wrapping :class:`RomManager` for tool/agent consumption."""
37
+
38
+ __slots__ = ("directory", "iso_type", "verbose", "force")
39
+
40
+ def __init__(
41
+ self,
42
+ directory: str = os.path.curdir,
43
+ iso_type: str = "chd",
44
+ verbose: bool = False,
45
+ force: bool = False,
46
+ ) -> None:
47
+ """Hold conversion defaults for the facade (CONCEPT:ROM-001)."""
48
+ self.directory = directory
49
+ self.iso_type = iso_type
50
+ self.verbose = verbose
51
+ self.force = force
52
+
53
+ def _build_manager(
54
+ self,
55
+ directory: str | None = None,
56
+ iso_type: str | None = None,
57
+ verbose: bool | None = None,
58
+ force: bool | None = None,
59
+ clean_origin_files: bool = False,
60
+ ) -> RomManager:
61
+ manager = RomManager()
62
+ manager.directory = directory if directory is not None else self.directory
63
+ manager.iso_type = iso_type if iso_type is not None else self.iso_type
64
+ manager.verbose = verbose if verbose is not None else self.verbose
65
+ manager.force = force if force is not None else self.force
66
+ manager.clean_origin_files = clean_origin_files
67
+ return manager
68
+
69
+ # --- Conversion domain -------------------------------------------------
70
+ def convert(
71
+ self,
72
+ directory: str | None = None,
73
+ cpu_count: int | None = None,
74
+ iso_type: str | None = None,
75
+ verbose: bool | None = None,
76
+ force: bool | None = None,
77
+ clean_origin_files: bool = False,
78
+ ) -> dict[str, Any]:
79
+ """Run the full parallel conversion pipeline over a directory (CONCEPT:ROM-001)."""
80
+ manager = self._build_manager(
81
+ directory=directory,
82
+ iso_type=iso_type,
83
+ verbose=verbose,
84
+ force=force,
85
+ clean_origin_files=clean_origin_files,
86
+ )
87
+ before = get_directory_size(directory=manager.directory)
88
+ results = manager.process_parallel(cpu_count=cpu_count)
89
+ after = get_directory_size(directory=manager.directory)
90
+ return {
91
+ "directory": manager.directory,
92
+ "iso_type": manager.iso_type,
93
+ "files_processed": len(results) if results else 0,
94
+ "size_before_gb": round(before[3], 4),
95
+ "size_after_gb": round(after[3], 4),
96
+ "storage_delta_gb": round(before[3] - after[3], 4),
97
+ }
98
+
99
+ # alias — convert an entire directory tree
100
+ process_directory = convert
101
+
102
+ def process_file(
103
+ self,
104
+ file: str,
105
+ iso_type: str | None = None,
106
+ verbose: bool | None = None,
107
+ force: bool | None = None,
108
+ ) -> dict[str, Any]:
109
+ """Process a single ROM file through the conversion pipeline (CONCEPT:ROM-001)."""
110
+ manager = self._build_manager(iso_type=iso_type, verbose=verbose, force=force)
111
+ manager.process_file(
112
+ file=file,
113
+ logger_name=manager.logger_name,
114
+ logger_level=logging.WARNING,
115
+ logger_format=manager.logger_format,
116
+ )
117
+ return {"file": file, "status": "processed"}
118
+
119
+ def generate_cue(self, directory: str) -> dict[str, Any]:
120
+ """Generate a missing ``.cue`` sheet for ``.bin`` tracks in a directory."""
121
+ manager = self._build_manager(directory=directory)
122
+ cue_path = manager.cue_file_generator(directory=directory)
123
+ return {"directory": directory, "cue_file": cue_path}
124
+
125
+ def list_files(self, directory: str, extensions: list[str] | None = None):
126
+ """List ROM/archive files under a directory matching extensions."""
127
+ manager = self._build_manager(directory=directory)
128
+ exts = tuple(extensions) if extensions else manager.supported_extensions
129
+ return manager.get_files(directory=directory, extensions=exts)
130
+
131
+ # --- Game codes domain -------------------------------------------------
132
+ @staticmethod
133
+ def lookup_game_code(code: str) -> dict[str, Any]:
134
+ """Look up a single game code -> name in the PSX code registry."""
135
+ return {"code": code, "name": psx_codes.get(code)}
136
+
137
+ @staticmethod
138
+ def list_game_codes(prefix: str | None = None) -> dict[str, str]:
139
+ """List known game codes, optionally filtered by a code prefix."""
140
+ if prefix:
141
+ return {k: v for k, v in psx_codes.items() if k.startswith(prefix)}
142
+ return dict(psx_codes)
143
+
144
+ def rename_by_game_code(self, file: str) -> dict[str, Any]:
145
+ """Rename a file in-place using the embedded game code, if recognised."""
146
+ renamed = RomManager.map_game_code_name(file=file, logger=logger)
147
+ return {"original": file, "renamed": renamed, "changed": renamed != file}
@@ -0,0 +1,21 @@
1
+ # IDENTITY.md - ROM Manager Agent Identity
2
+
3
+ ## [default]
4
+ * **Name:** ROM Manager
5
+ * **Role:** Agent for converting and organizing game ROMs (CHD/RVZ)
6
+ * **Emoji:** 👾
7
+
8
+ ### System Prompt
9
+ You are the ROM Manager Agent.
10
+ You must always first run `list_skills` to show all skills.
11
+ Then, use the `mcp-client` universal skill and check the reference documentation for `rom-manager.md` to discover the exact tags and tools available for your capabilities.
12
+
13
+ ### Capabilities
14
+ - **ROM Conversion**: Convert game ROMs to CHD (`chdman`) or RVZ (`dolphin-tool`), extract archives, and generate `.cue` sheets via the `conversion` tool domain (CONCEPT:ROM-001).
15
+ - **Game Codes / Naming**: Resolve game/serial codes to titles and rename ROM files using the embedded code registry via the `game-codes` tool domain (CONCEPT:ROM-002).
16
+ - **MCP Operations**: Leverage the `mcp-client` skill to interact with the target MCP server. Refer to `rom-manager.md` for specific tool capabilities.
17
+ - **Custom Agent**: Handle custom tasks or general tasks.
18
+
19
+ ### Safety
20
+ - Always recommend backing up ROMs before running destructive (`clean_origin_files`/delete) operations.
21
+ - Verify the external `chdman` / `dolphin-tool` binaries are installed before attempting conversions.
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/python
2
+ """ROM Manager A2A agent server.
3
+
4
+ CONCEPT:ROM-001, CONCEPT:ROM-002 — exposes the ROM conversion and game-codes
5
+ tool domains over an A2A/AG-UI surface. The agent is driven by the
6
+ agent-utilities router, the integration point for the cross-project bridge
7
+ capabilities CONCEPT:ORCH-1.2 (Confidence-Gated Router), CONCEPT:OS-5.2
8
+ (Cognitive Scheduler) and CONCEPT:KG-2.0 (Knowledge Graph Core ingestion of the
9
+ tool surface).
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import sys
15
+ import warnings
16
+
17
+ __version__ = "1.0.0"
18
+
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
22
+ handlers=[logging.StreamHandler()],
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ DEFAULT_AGENT_NAME = None
28
+ DEFAULT_AGENT_DESCRIPTION = None
29
+ DEFAULT_AGENT_SYSTEM_PROMPT = None
30
+
31
+
32
+ def agent_server():
33
+ """Launch the ROM Manager A2A agent (CONCEPT:ROM-001, CONCEPT:ORCH-1.2)."""
34
+ from agent_utilities import (
35
+ build_system_prompt_from_workspace,
36
+ create_agent_parser,
37
+ create_agent_server,
38
+ initialize_workspace,
39
+ load_identity,
40
+ )
41
+
42
+ global DEFAULT_AGENT_NAME, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_SYSTEM_PROMPT
43
+ initialize_workspace()
44
+ meta = load_identity()
45
+ DEFAULT_AGENT_NAME = os.getenv(
46
+ "DEFAULT_AGENT_NAME", meta.get("name", "ROM Manager")
47
+ )
48
+ DEFAULT_AGENT_DESCRIPTION = os.getenv(
49
+ "AGENT_DESCRIPTION",
50
+ meta.get(
51
+ "description",
52
+ "AI agent for converting and organizing game ROMs (CHD/RVZ).",
53
+ ),
54
+ )
55
+ DEFAULT_AGENT_SYSTEM_PROMPT = os.getenv(
56
+ "AGENT_SYSTEM_PROMPT",
57
+ meta.get("content") or build_system_prompt_from_workspace(),
58
+ )
59
+
60
+ warnings.filterwarnings("ignore", message=".*urllib3.*or chardet.*")
61
+ warnings.filterwarnings("ignore", category=DeprecationWarning, module="fastmcp")
62
+
63
+ print(f"{DEFAULT_AGENT_NAME} v{__version__}", file=sys.stderr)
64
+ parser = create_agent_parser()
65
+ args = parser.parse_args()
66
+
67
+ if args.debug:
68
+ logging.getLogger().setLevel(logging.DEBUG)
69
+ logger.debug("Debug mode enabled")
70
+
71
+ # Start server using the auto-discovery pattern (from mcp_config.json)
72
+ create_agent_server(
73
+ mcp_url=args.mcp_url,
74
+ mcp_config=args.mcp_config or "mcp_config.json",
75
+ host=args.host,
76
+ port=args.port,
77
+ provider=args.provider,
78
+ model_id=args.model_id,
79
+ router_model=args.model_id,
80
+ agent_model=args.model_id,
81
+ base_url=args.base_url,
82
+ api_key=args.api_key,
83
+ custom_skills_directory=args.custom_skills_directory,
84
+ enable_web_ui=args.web,
85
+ enable_otel=args.otel,
86
+ otel_endpoint=args.otel_endpoint,
87
+ otel_headers=args.otel_headers,
88
+ otel_public_key=args.otel_public_key,
89
+ otel_secret_key=args.otel_secret_key,
90
+ otel_protocol=args.otel_protocol,
91
+ debug=args.debug,
92
+ )
93
+
94
+
95
+ if __name__ == "__main__":
96
+ agent_server()
@@ -0,0 +1,22 @@
1
+ """Backwards-compatible shim re-exporting the API facade adapter.
2
+
3
+ The real implementation now lives in :mod:`rom_manager.adapters.api_client` (the
4
+ adapter layer). This shim preserves the historical ``rom_manager.api_client``
5
+ import path used by ``auth.get_client`` and the MCP tools. CONCEPT:ROM-001.
6
+ """
7
+
8
+ from rom_manager.adapters.api_client import (
9
+ Api,
10
+ RomManager,
11
+ get_directory_size,
12
+ get_operating_system,
13
+ psx_codes,
14
+ )
15
+
16
+ __all__ = [
17
+ "Api",
18
+ "RomManager",
19
+ "psx_codes",
20
+ "get_operating_system",
21
+ "get_directory_size",
22
+ ]
@@ -0,0 +1,24 @@
1
+ """Backwards-compatible shim re-exporting the archives domain module.
2
+
3
+ The real implementation now lives in :mod:`rom_manager.core.archives` (the
4
+ domain layer). This shim preserves the historical ``rom_manager.archives``
5
+ import path. CONCEPT:ROM-001.
6
+ """
7
+
8
+ from rom_manager.core.archives import (
9
+ ARCHIVE_FORMATS,
10
+ cue_file_generator,
11
+ extract_archive,
12
+ get_files,
13
+ is_archive,
14
+ pad_leading_zero,
15
+ )
16
+
17
+ __all__ = [
18
+ "ARCHIVE_FORMATS",
19
+ "cue_file_generator",
20
+ "extract_archive",
21
+ "get_files",
22
+ "is_archive",
23
+ "pad_leading_zero",
24
+ ]
rom_manager/auth.py ADDED
@@ -0,0 +1,47 @@
1
+ """ROM Manager local configuration / client factory.
2
+
3
+ Unlike network-backed connectors (e.g. ``gitlab-api``), ``rom-manager`` is a
4
+ **local CLI tool**: it extracts archives, renames ROMs via known game codes, and
5
+ converts ISO/WBFS images to CHD/RVZ using locally-installed binaries
6
+ (``chdman`` / ``dolphin-tool``). There is therefore **no service URL and no
7
+ required credentials**.
8
+
9
+ This module mirrors the ecosystem ``auth.py`` shape (a ``get_client`` factory
10
+ the MCP tools depend on) but degrades to a no-op/local configuration. The only
11
+ recognised setting is the optional ``ROM_DIRECTORY`` environment variable, used
12
+ as the default working directory when none is supplied.
13
+ """
14
+
15
+ import os
16
+
17
+ from agent_utilities.base_utilities import get_logger, to_boolean
18
+
19
+ from rom_manager.api_client import Api
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ def get_client(
25
+ directory: str | None = None,
26
+ iso_type: str = os.getenv("ROM_ISO_TYPE", "chd"),
27
+ verbose: bool = to_boolean(string=os.getenv("ROM_VERBOSE", "False")),
28
+ force: bool = to_boolean(string=os.getenv("ROM_FORCE", "False")),
29
+ config: dict | None = None,
30
+ ) -> Api:
31
+ """Factory returning the local ROM Manager :class:`Api` facade (CONCEPT:ROM-001).
32
+
33
+ No credentials are required. ``directory`` defaults to ``ROM_DIRECTORY`` (or
34
+ the current working directory). ``iso_type`` selects the conversion target
35
+ (``chd`` or ``rvz``).
36
+ """
37
+ resolved_directory = directory or os.getenv("ROM_DIRECTORY") or os.path.curdir
38
+ logger.info(
39
+ "Creating local ROM Manager client",
40
+ extra={"directory": resolved_directory, "iso_type": iso_type},
41
+ )
42
+ return Api(
43
+ directory=resolved_directory,
44
+ iso_type=iso_type if iso_type in ("chd", "rvz") else "chd",
45
+ verbose=verbose,
46
+ force=force,
47
+ )
@@ -0,0 +1,15 @@
1
+ """Backwards-compatible shim re-exporting the conversion domain module.
2
+
3
+ The real implementation now lives in :mod:`rom_manager.core.conversion` (the
4
+ domain layer). This shim preserves the historical ``rom_manager.conversion``
5
+ import path. CONCEPT:ROM-001.
6
+ """
7
+
8
+ from rom_manager.core.conversion import (
9
+ Converter,
10
+ Runner,
11
+ build_convert_command,
12
+ run_command,
13
+ )
14
+
15
+ __all__ = ["Converter", "Runner", "build_convert_command", "run_command"]
@@ -0,0 +1,12 @@
1
+ """Domain (core) layer for ROM Manager.
2
+
3
+ CONCEPT:ROM-001, CONCEPT:ROM-002 — the pure pipeline responsibilities, free of
4
+ transport/CLI concerns: archive handling, conversion command construction, and
5
+ game-code naming. The top-level orchestrator (:class:`rom_manager.rom_manager.
6
+ RomManager`) composes these. Thin re-export shims at the package root
7
+ (``rom_manager.archives`` etc.) preserve the historical import paths.
8
+ """
9
+
10
+ from rom_manager.core import archives, conversion, naming
11
+
12
+ __all__ = ["archives", "conversion", "naming"]
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python
2
+ """Archive extraction and ``.cue`` sheet generation.
3
+
4
+ CONCEPT:ROM-001 — ROM Conversion (pre-processing stage). Owns the archive
5
+ responsibilities split out of the former ``rom_manager`` god-module: detecting
6
+ supported archive formats, extracting them (via the optional ``patool`` native
7
+ extra), and synthesising missing ``.cue`` sheets from ``.bin`` tracks.
8
+ """
9
+
10
+ import glob
11
+ import logging
12
+ import os
13
+
14
+ # Supported archive container formats (extraction inputs).
15
+ ARCHIVE_FORMATS: tuple[str, ...] = (
16
+ ".7z",
17
+ ".zip",
18
+ ".tar.gz",
19
+ ".gz",
20
+ ".gzip",
21
+ ".bz2",
22
+ ".bzip2",
23
+ ".rar",
24
+ ".tar",
25
+ )
26
+
27
+ _NULL_LOGGER = logging.getLogger("rom_manager.archives")
28
+ _NULL_LOGGER.addHandler(logging.NullHandler())
29
+
30
+
31
+ def is_archive(file: str) -> bool:
32
+ """Return ``True`` if ``file`` has a supported archive extension."""
33
+ return file.lower().endswith(ARCHIVE_FORMATS)
34
+
35
+
36
+ def get_files(directory, extensions) -> list[str]:
37
+ """Recursively list files under ``directory`` matching ``extensions``."""
38
+ matching_files = []
39
+ for root, _dirs, files in os.walk(directory):
40
+ for file in files:
41
+ if any(file.endswith(ext) for ext in extensions):
42
+ matching_files.append(os.path.join(root, file))
43
+ return matching_files
44
+
45
+
46
+ def pad_leading_zero(number) -> str:
47
+ """Zero-pad ``number`` to a two-character track index (e.g. ``2`` -> ``02``)."""
48
+ padded = "0" + str(number)
49
+ return padded[-2:]
50
+
51
+
52
+ def cue_file_generator(directory, logger=None) -> str:
53
+ """Generate a ``.cue`` sheet from the ``.bin`` tracks in ``directory``.
54
+
55
+ CONCEPT:ROM-001 — emits a standard CD cue sheet (track 01 ``MODE2/2352``,
56
+ subsequent tracks ``AUDIO``) so ``chdman`` can ingest multi-track ``.bin``
57
+ images. Returns the path to the (existing or newly written) cue file.
58
+ """
59
+ file_names = get_files(directory=directory, extensions=[".bin"])
60
+ first_file = file_names.pop(0)
61
+ first_file = os.path.basename(first_file)
62
+ sheet = (
63
+ f'FILE "{first_file}" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n'
64
+ )
65
+ track_counter = 2
66
+ for file_name in file_names:
67
+ sheet += (
68
+ f'FILE "{file_name}" BINARY\n'
69
+ f" TRACK {pad_leading_zero(track_counter)} AUDIO\n"
70
+ f" INDEX 00 00:00:00\n"
71
+ f" INDEX 01 00:02:00\n"
72
+ )
73
+ track_counter += 1
74
+ cue_file_path = os.path.join(
75
+ directory, f"{os.path.splitext(os.path.basename(first_file))[0]}.cue"
76
+ )
77
+ if not os.path.exists(cue_file_path):
78
+ with open(cue_file_path, "w") as cue_file:
79
+ cue_file.write(sheet)
80
+ return cue_file_path
81
+
82
+
83
+ def extract_archive(archive, archive_directory, verbose=False, logger=None) -> None:
84
+ """Extract ``archive`` into ``archive_directory`` and backfill cue sheets.
85
+
86
+ CONCEPT:ROM-001 — the archive pre-processing stage of the conversion
87
+ pipeline. ``patool`` is imported lazily so the core package installs without
88
+ native extras; install with ``rom-manager[native]`` for extraction.
89
+ """
90
+ log = logger or _NULL_LOGGER
91
+ try:
92
+ import patoolib as patool
93
+ except ImportError as e: # pragma: no cover - optional native dependency
94
+ raise ImportError(
95
+ "Archive extraction requires the 'patool' library. "
96
+ "Install the native extras with: pip install 'rom-manager[native]'"
97
+ ) from e
98
+
99
+ log.info(f"Extracting {archive} to {archive_directory}...")
100
+ verbosity = 1 if verbose else -1
101
+ try:
102
+ patool.extract_archive(archive, outdir=archive_directory, verbosity=verbosity)
103
+ except patool.util.PatoolError as e:
104
+ log.info(f"Unable to extract: {archive}\nError: {e}")
105
+
106
+ log.info(f"Finished extracting {archive} to {archive_directory}")
107
+ log.info("Generating any missing cue file(s)")
108
+ if glob.glob(os.path.join(str(archive_directory), "*.bin")) and not glob.glob(
109
+ os.path.join(str(archive_directory), "*.cue")
110
+ ):
111
+ cue_file_generator(archive_directory, logger=log)
112
+ log.info("Finished generating missing cue file(s)")