hatch-xclam 0.7.1.dev3__py3-none-any.whl → 0.8.0.dev1__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.
Files changed (81) hide show
  1. hatch/__init__.py +1 -1
  2. hatch/cli/__init__.py +71 -0
  3. hatch/cli/__main__.py +1035 -0
  4. hatch/cli/cli_env.py +865 -0
  5. hatch/cli/cli_mcp.py +1965 -0
  6. hatch/cli/cli_package.py +566 -0
  7. hatch/cli/cli_system.py +136 -0
  8. hatch/cli/cli_utils.py +1289 -0
  9. hatch/cli_hatch.py +160 -2838
  10. hatch/mcp_host_config/__init__.py +10 -10
  11. hatch/mcp_host_config/adapters/__init__.py +34 -0
  12. hatch/mcp_host_config/adapters/base.py +170 -0
  13. hatch/mcp_host_config/adapters/claude.py +105 -0
  14. hatch/mcp_host_config/adapters/codex.py +104 -0
  15. hatch/mcp_host_config/adapters/cursor.py +83 -0
  16. hatch/mcp_host_config/adapters/gemini.py +75 -0
  17. hatch/mcp_host_config/adapters/kiro.py +78 -0
  18. hatch/mcp_host_config/adapters/lmstudio.py +79 -0
  19. hatch/mcp_host_config/adapters/registry.py +149 -0
  20. hatch/mcp_host_config/adapters/vscode.py +83 -0
  21. hatch/mcp_host_config/backup.py +5 -3
  22. hatch/mcp_host_config/fields.py +126 -0
  23. hatch/mcp_host_config/models.py +161 -456
  24. hatch/mcp_host_config/reporting.py +57 -16
  25. hatch/mcp_host_config/strategies.py +155 -87
  26. hatch/template_generator.py +1 -1
  27. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
  28. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
  29. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
  30. hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
  31. tests/cli_test_utils.py +280 -0
  32. tests/integration/cli/__init__.py +14 -0
  33. tests/integration/cli/test_cli_reporter_integration.py +2439 -0
  34. tests/integration/mcp/__init__.py +0 -0
  35. tests/integration/mcp/test_adapter_serialization.py +173 -0
  36. tests/regression/cli/__init__.py +16 -0
  37. tests/regression/cli/test_color_logic.py +268 -0
  38. tests/regression/cli/test_consequence_type.py +298 -0
  39. tests/regression/cli/test_error_formatting.py +328 -0
  40. tests/regression/cli/test_result_reporter.py +586 -0
  41. tests/regression/cli/test_table_formatter.py +211 -0
  42. tests/regression/mcp/__init__.py +0 -0
  43. tests/regression/mcp/test_field_filtering.py +162 -0
  44. tests/test_cli_version.py +7 -5
  45. tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
  46. tests/unit/__init__.py +0 -0
  47. tests/unit/mcp/__init__.py +0 -0
  48. tests/unit/mcp/test_adapter_protocol.py +138 -0
  49. tests/unit/mcp/test_adapter_registry.py +158 -0
  50. tests/unit/mcp/test_config_model.py +146 -0
  51. hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
  52. tests/integration/test_mcp_kiro_integration.py +0 -153
  53. tests/regression/test_mcp_codex_backup_integration.py +0 -162
  54. tests/regression/test_mcp_codex_host_strategy.py +0 -163
  55. tests/regression/test_mcp_codex_model_validation.py +0 -117
  56. tests/regression/test_mcp_kiro_backup_integration.py +0 -241
  57. tests/regression/test_mcp_kiro_cli_integration.py +0 -141
  58. tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
  59. tests/regression/test_mcp_kiro_host_strategy.py +0 -214
  60. tests/regression/test_mcp_kiro_model_validation.py +0 -116
  61. tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
  62. tests/test_mcp_atomic_operations.py +0 -276
  63. tests/test_mcp_backup_integration.py +0 -308
  64. tests/test_mcp_cli_all_host_specific_args.py +0 -496
  65. tests/test_mcp_cli_backup_management.py +0 -295
  66. tests/test_mcp_cli_direct_management.py +0 -456
  67. tests/test_mcp_cli_discovery_listing.py +0 -582
  68. tests/test_mcp_cli_host_config_integration.py +0 -823
  69. tests/test_mcp_cli_package_management.py +0 -360
  70. tests/test_mcp_cli_partial_updates.py +0 -859
  71. tests/test_mcp_environment_integration.py +0 -520
  72. tests/test_mcp_host_config_backup.py +0 -257
  73. tests/test_mcp_host_configuration_manager.py +0 -331
  74. tests/test_mcp_host_registry_decorator.py +0 -348
  75. tests/test_mcp_pydantic_architecture_v4.py +0 -603
  76. tests/test_mcp_server_config_models.py +0 -242
  77. tests/test_mcp_server_config_type_field.py +0 -221
  78. tests/test_mcp_sync_functionality.py +0 -316
  79. tests/test_mcp_user_feedback_reporting.py +0 -359
  80. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
  81. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/top_level.txt +0 -0
@@ -17,17 +17,26 @@ import logging
17
17
  from .host_management import MCPHostStrategy, register_host_strategy
18
18
  from .models import MCPHostType, MCPServerConfig, HostConfiguration
19
19
  from .backup import MCPHostConfigBackupManager, AtomicFileOperations
20
+ from .adapters import get_adapter
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
23
24
 
24
25
  class ClaudeHostStrategy(MCPHostStrategy):
25
26
  """Base strategy for Claude family hosts with shared patterns."""
26
-
27
+
27
28
  def __init__(self):
28
29
  self.company_origin = "Anthropic"
29
30
  self.config_format = "claude_format"
30
-
31
+
32
+ def get_adapter_host_name(self) -> str:
33
+ """Return the adapter host name for this strategy.
34
+
35
+ Subclasses should override to return their specific adapter host name.
36
+ Default is 'claude-desktop' for backward compatibility.
37
+ """
38
+ return "claude-desktop"
39
+
31
40
  def get_config_key(self) -> str:
32
41
  """Claude family uses 'mcpServers' key."""
33
42
  return "mcpServers"
@@ -85,15 +94,15 @@ class ClaudeHostStrategy(MCPHostStrategy):
85
94
  return HostConfiguration()
86
95
 
87
96
  def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
88
- """Write Claude configuration file."""
97
+ """Write Claude configuration file using adapter-based serialization."""
89
98
  config_path = self.get_config_path()
90
99
  if not config_path:
91
100
  return False
92
-
101
+
93
102
  try:
94
103
  # Ensure parent directory exists
95
104
  config_path.parent.mkdir(parents=True, exist_ok=True)
96
-
105
+
97
106
  # Read existing configuration to preserve non-MCP settings
98
107
  existing_config = {}
99
108
  if config_path.exists():
@@ -102,23 +111,24 @@ class ClaudeHostStrategy(MCPHostStrategy):
102
111
  existing_config = json.load(f)
103
112
  except Exception:
104
113
  pass # Start with empty config if read fails
105
-
106
- # Convert MCPServerConfig objects to dict
114
+
115
+ # Use adapter for serialization (includes validation and field filtering)
116
+ adapter = get_adapter(self.get_adapter_host_name())
107
117
  servers_dict = {}
108
118
  for name, server_config in config.servers.items():
109
- servers_dict[name] = server_config.model_dump(exclude_none=True)
110
-
119
+ servers_dict[name] = adapter.serialize(server_config)
120
+
111
121
  # Preserve Claude-specific settings
112
122
  updated_config = self._preserve_claude_settings(existing_config, servers_dict)
113
-
123
+
114
124
  # Write atomically
115
125
  temp_path = config_path.with_suffix('.tmp')
116
126
  with open(temp_path, 'w') as f:
117
127
  json.dump(updated_config, f, indent=2)
118
-
128
+
119
129
  temp_path.replace(config_path)
120
130
  return True
121
-
131
+
122
132
  except Exception as e:
123
133
  logger.error(f"Failed to write Claude configuration: {e}")
124
134
  return False
@@ -149,13 +159,17 @@ class ClaudeDesktopStrategy(ClaudeHostStrategy):
149
159
  @register_host_strategy(MCPHostType.CLAUDE_CODE)
150
160
  class ClaudeCodeStrategy(ClaudeHostStrategy):
151
161
  """Configuration strategy for Claude for VS Code."""
152
-
162
+
163
+ def get_adapter_host_name(self) -> str:
164
+ """Return the adapter host name for Claude Code."""
165
+ return "claude-code"
166
+
153
167
  def get_config_path(self) -> Optional[Path]:
154
168
  """Get Claude Code configuration path (workspace-specific)."""
155
169
  # Claude Code uses workspace-specific configuration
156
170
  # This would be determined at runtime based on current workspace
157
171
  return Path.home() / ".claude.json"
158
-
172
+
159
173
  def is_host_available(self) -> bool:
160
174
  """Check if Claude Code is available."""
161
175
  # Check for Claude Code user configuration file
@@ -165,15 +179,22 @@ class ClaudeCodeStrategy(ClaudeHostStrategy):
165
179
 
166
180
  class CursorBasedHostStrategy(MCPHostStrategy):
167
181
  """Base strategy for Cursor-based hosts (Cursor and LM Studio)."""
168
-
182
+
169
183
  def __init__(self):
170
184
  self.config_format = "cursor_format"
171
185
  self.supports_remote_servers = True
172
-
186
+
187
+ def get_adapter_host_name(self) -> str:
188
+ """Return the adapter host name for this strategy.
189
+
190
+ Subclasses should override. Default is 'cursor'.
191
+ """
192
+ return "cursor"
193
+
173
194
  def get_config_key(self) -> str:
174
195
  """Cursor family uses 'mcpServers' key."""
175
196
  return "mcpServers"
176
-
197
+
177
198
  def validate_server_config(self, server_config: MCPServerConfig) -> bool:
178
199
  """Cursor family validation - supports both local and remote servers."""
179
200
  # Cursor family is more flexible with paths and supports remote servers
@@ -182,11 +203,14 @@ class CursorBasedHostStrategy(MCPHostStrategy):
182
203
  elif server_config.url:
183
204
  return True # Remote server
184
205
  return False
185
-
206
+
186
207
  def _format_cursor_server_config(self, server_config: MCPServerConfig) -> Dict:
187
- """Format server configuration for Cursor family."""
208
+ """Format server configuration for Cursor family.
209
+
210
+ DEPRECATED: Use adapter.serialize() instead.
211
+ """
188
212
  config = {}
189
-
213
+
190
214
  if server_config.command:
191
215
  # Local server configuration
192
216
  config["command"] = server_config.command
@@ -199,22 +223,22 @@ class CursorBasedHostStrategy(MCPHostStrategy):
199
223
  config["url"] = server_config.url
200
224
  if server_config.headers:
201
225
  config["headers"] = server_config.headers
202
-
226
+
203
227
  return config
204
-
228
+
205
229
  def read_configuration(self) -> HostConfiguration:
206
230
  """Read Cursor-based configuration file."""
207
231
  config_path = self.get_config_path()
208
232
  if not config_path or not config_path.exists():
209
233
  return HostConfiguration()
210
-
234
+
211
235
  try:
212
236
  with open(config_path, 'r') as f:
213
237
  config_data = json.load(f)
214
-
238
+
215
239
  # Extract MCP servers
216
240
  mcp_servers = config_data.get(self.get_config_key(), {})
217
-
241
+
218
242
  # Convert to MCPServerConfig objects
219
243
  servers = {}
220
244
  for name, server_data in mcp_servers.items():
@@ -223,23 +247,23 @@ class CursorBasedHostStrategy(MCPHostStrategy):
223
247
  except Exception as e:
224
248
  logger.warning(f"Invalid server config for {name}: {e}")
225
249
  continue
226
-
250
+
227
251
  return HostConfiguration(servers=servers)
228
-
252
+
229
253
  except Exception as e:
230
254
  logger.error(f"Failed to read Cursor configuration: {e}")
231
255
  return HostConfiguration()
232
-
256
+
233
257
  def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
234
- """Write Cursor-based configuration file."""
258
+ """Write Cursor-based configuration file using adapter-based serialization."""
235
259
  config_path = self.get_config_path()
236
260
  if not config_path:
237
261
  return False
238
-
262
+
239
263
  try:
240
264
  # Ensure parent directory exists
241
265
  config_path.parent.mkdir(parents=True, exist_ok=True)
242
-
266
+
243
267
  # Read existing configuration
244
268
  existing_config = {}
245
269
  if config_path.exists():
@@ -248,23 +272,24 @@ class CursorBasedHostStrategy(MCPHostStrategy):
248
272
  existing_config = json.load(f)
249
273
  except Exception:
250
274
  pass
251
-
252
- # Convert MCPServerConfig objects to dict
275
+
276
+ # Use adapter for serialization (includes validation and field filtering)
277
+ adapter = get_adapter(self.get_adapter_host_name())
253
278
  servers_dict = {}
254
279
  for name, server_config in config.servers.items():
255
- servers_dict[name] = server_config.model_dump(exclude_none=True)
256
-
280
+ servers_dict[name] = adapter.serialize(server_config)
281
+
257
282
  # Update configuration
258
283
  existing_config[self.get_config_key()] = servers_dict
259
-
284
+
260
285
  # Write atomically
261
286
  temp_path = config_path.with_suffix('.tmp')
262
287
  with open(temp_path, 'w') as f:
263
288
  json.dump(existing_config, f, indent=2)
264
-
289
+
265
290
  temp_path.replace(config_path)
266
291
  return True
267
-
292
+
268
293
  except Exception as e:
269
294
  logger.error(f"Failed to write Cursor configuration: {e}")
270
295
  return False
@@ -287,11 +312,15 @@ class CursorHostStrategy(CursorBasedHostStrategy):
287
312
  @register_host_strategy(MCPHostType.LMSTUDIO)
288
313
  class LMStudioHostStrategy(CursorBasedHostStrategy):
289
314
  """Configuration strategy for LM Studio (follows Cursor format)."""
290
-
315
+
316
+ def get_adapter_host_name(self) -> str:
317
+ """Return the adapter host name for LM Studio."""
318
+ return "lmstudio"
319
+
291
320
  def get_config_path(self) -> Optional[Path]:
292
321
  """Get LM Studio configuration path."""
293
322
  return Path.home() / ".lmstudio" / "mcp.json"
294
-
323
+
295
324
  def is_host_available(self) -> bool:
296
325
  """Check if LM Studio is installed."""
297
326
  config_path = self.get_config_path()
@@ -302,6 +331,10 @@ class LMStudioHostStrategy(CursorBasedHostStrategy):
302
331
  class VSCodeHostStrategy(MCPHostStrategy):
303
332
  """Configuration strategy for VS Code MCP extension with user-wide mcp support."""
304
333
 
334
+ def get_adapter_host_name(self) -> str:
335
+ """Return the adapter host name for VS Code."""
336
+ return "vscode"
337
+
305
338
  def get_config_path(self) -> Optional[Path]:
306
339
  """Get VS Code user mcp configuration path (cross-platform)."""
307
340
  try:
@@ -343,7 +376,7 @@ class VSCodeHostStrategy(MCPHostStrategy):
343
376
  def validate_server_config(self, server_config: MCPServerConfig) -> bool:
344
377
  """VS Code validation - flexible path handling."""
345
378
  return server_config.command is not None or server_config.url is not None
346
-
379
+
347
380
  def read_configuration(self) -> HostConfiguration:
348
381
  """Read VS Code mcp.json configuration."""
349
382
  config_path = self.get_config_path()
@@ -373,7 +406,7 @@ class VSCodeHostStrategy(MCPHostStrategy):
373
406
  return HostConfiguration()
374
407
 
375
408
  def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
376
- """Write VS Code mcp.json configuration."""
409
+ """Write VS Code mcp.json configuration using adapter-based serialization."""
377
410
  config_path = self.get_config_path()
378
411
  if not config_path:
379
412
  return False
@@ -391,10 +424,11 @@ class VSCodeHostStrategy(MCPHostStrategy):
391
424
  except Exception:
392
425
  pass
393
426
 
394
- # Convert MCPServerConfig objects to dict
427
+ # Use adapter for serialization (includes validation and field filtering)
428
+ adapter = get_adapter(self.get_adapter_host_name())
395
429
  servers_dict = {}
396
430
  for name, server_config in config.servers.items():
397
- servers_dict[name] = server_config.model_dump(exclude_none=True)
431
+ servers_dict[name] = adapter.serialize(server_config)
398
432
 
399
433
  # Update configuration with new servers (preserves non-MCP settings)
400
434
  existing_config[self.get_config_key()] = servers_dict
@@ -415,83 +449,88 @@ class VSCodeHostStrategy(MCPHostStrategy):
415
449
  @register_host_strategy(MCPHostType.KIRO)
416
450
  class KiroHostStrategy(MCPHostStrategy):
417
451
  """Configuration strategy for Kiro IDE."""
418
-
452
+
453
+ def get_adapter_host_name(self) -> str:
454
+ """Return the adapter host name for Kiro."""
455
+ return "kiro"
456
+
419
457
  def get_config_path(self) -> Optional[Path]:
420
458
  """Get Kiro configuration path (user-level only per constraint)."""
421
459
  return Path.home() / ".kiro" / "settings" / "mcp.json"
422
-
460
+
423
461
  def get_config_key(self) -> str:
424
462
  """Kiro uses 'mcpServers' key."""
425
463
  return "mcpServers"
426
-
464
+
427
465
  def is_host_available(self) -> bool:
428
466
  """Check if Kiro is available by checking for settings directory."""
429
467
  kiro_dir = Path.home() / ".kiro" / "settings"
430
468
  return kiro_dir.exists()
431
-
469
+
432
470
  def validate_server_config(self, server_config: MCPServerConfig) -> bool:
433
471
  """Kiro validation - supports both local and remote servers."""
434
472
  return server_config.command is not None or server_config.url is not None
435
-
473
+
436
474
  def read_configuration(self) -> HostConfiguration:
437
475
  """Read Kiro configuration file."""
438
476
  config_path_str = self.get_config_path()
439
477
  if not config_path_str:
440
478
  return HostConfiguration(servers={})
441
-
479
+
442
480
  config_path = Path(config_path_str)
443
481
  if not config_path.exists():
444
482
  return HostConfiguration(servers={})
445
-
483
+
446
484
  try:
447
485
  with open(config_path, 'r', encoding='utf-8') as f:
448
486
  data = json.load(f)
449
-
487
+
450
488
  servers = {}
451
489
  mcp_servers = data.get(self.get_config_key(), {})
452
-
490
+
453
491
  for name, config in mcp_servers.items():
454
492
  try:
455
493
  servers[name] = MCPServerConfig(**config)
456
494
  except Exception as e:
457
495
  logger.warning(f"Invalid server config for {name}: {e}")
458
496
  continue
459
-
497
+
460
498
  return HostConfiguration(servers=servers)
461
-
499
+
462
500
  except Exception as e:
463
501
  logger.error(f"Failed to read Kiro configuration: {e}")
464
502
  return HostConfiguration(servers={})
465
-
503
+
466
504
  def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
467
- """Write configuration to Kiro with backup support."""
505
+ """Write configuration to Kiro with backup support using adapter-based serialization."""
468
506
  config_path_str = self.get_config_path()
469
507
  if not config_path_str:
470
508
  return False
471
-
509
+
472
510
  config_path = Path(config_path_str)
473
-
511
+
474
512
  try:
475
513
  # Ensure directory exists
476
514
  config_path.parent.mkdir(parents=True, exist_ok=True)
477
-
515
+
478
516
  # Read existing configuration to preserve other settings
479
517
  existing_data = {}
480
518
  if config_path.exists():
481
519
  with open(config_path, 'r', encoding='utf-8') as f:
482
520
  existing_data = json.load(f)
483
-
484
- # Update MCP servers section
521
+
522
+ # Use adapter for serialization (includes validation and field filtering)
523
+ adapter = get_adapter(self.get_adapter_host_name())
485
524
  servers_data = {}
486
525
  for name, server_config in config.servers.items():
487
- servers_data[name] = server_config.model_dump(exclude_unset=True)
488
-
526
+ servers_data[name] = adapter.serialize(server_config)
527
+
489
528
  existing_data[self.get_config_key()] = servers_data
490
-
529
+
491
530
  # Use atomic write with backup support
492
531
  backup_manager = MCPHostConfigBackupManager()
493
532
  atomic_ops = AtomicFileOperations()
494
-
533
+
495
534
  atomic_ops.atomic_write_with_backup(
496
535
  file_path=config_path,
497
536
  data=existing_data,
@@ -499,9 +538,9 @@ class KiroHostStrategy(MCPHostStrategy):
499
538
  hostname="kiro",
500
539
  skip_backup=no_backup
501
540
  )
502
-
541
+
503
542
  return True
504
-
543
+
505
544
  except Exception as e:
506
545
  logger.error(f"Failed to write Kiro configuration: {e}")
507
546
  return False
@@ -510,40 +549,44 @@ class KiroHostStrategy(MCPHostStrategy):
510
549
  @register_host_strategy(MCPHostType.GEMINI)
511
550
  class GeminiHostStrategy(MCPHostStrategy):
512
551
  """Configuration strategy for Google Gemini CLI MCP integration."""
513
-
552
+
553
+ def get_adapter_host_name(self) -> str:
554
+ """Return the adapter host name for Gemini."""
555
+ return "gemini"
556
+
514
557
  def get_config_path(self) -> Optional[Path]:
515
558
  """Get Gemini configuration path based on official documentation."""
516
559
  # Based on official Gemini CLI documentation: ~/.gemini/settings.json
517
560
  return Path.home() / ".gemini" / "settings.json"
518
-
561
+
519
562
  def get_config_key(self) -> str:
520
563
  """Gemini uses 'mcpServers' key in settings.json."""
521
564
  return "mcpServers"
522
-
565
+
523
566
  def is_host_available(self) -> bool:
524
567
  """Check if Gemini CLI is available."""
525
568
  # Check if Gemini CLI directory exists
526
569
  gemini_dir = Path.home() / ".gemini"
527
570
  return gemini_dir.exists()
528
-
571
+
529
572
  def validate_server_config(self, server_config: MCPServerConfig) -> bool:
530
573
  """Gemini validation - supports both local and remote servers."""
531
574
  # Gemini CLI supports both command-based and URL-based servers
532
575
  return server_config.command is not None or server_config.url is not None
533
-
576
+
534
577
  def read_configuration(self) -> HostConfiguration:
535
578
  """Read Gemini settings.json configuration."""
536
579
  config_path = self.get_config_path()
537
580
  if not config_path or not config_path.exists():
538
581
  return HostConfiguration()
539
-
582
+
540
583
  try:
541
584
  with open(config_path, 'r') as f:
542
585
  config_data = json.load(f)
543
-
586
+
544
587
  # Extract MCP servers from Gemini configuration
545
588
  mcp_servers = config_data.get(self.get_config_key(), {})
546
-
589
+
547
590
  # Convert to MCPServerConfig objects
548
591
  servers = {}
549
592
  for name, server_data in mcp_servers.items():
@@ -552,15 +595,15 @@ class GeminiHostStrategy(MCPHostStrategy):
552
595
  except Exception as e:
553
596
  logger.warning(f"Invalid server config for {name}: {e}")
554
597
  continue
555
-
598
+
556
599
  return HostConfiguration(servers=servers)
557
-
600
+
558
601
  except Exception as e:
559
602
  logger.error(f"Failed to read Gemini configuration: {e}")
560
603
  return HostConfiguration()
561
-
604
+
562
605
  def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
563
- """Write Gemini settings.json configuration."""
606
+ """Write Gemini settings.json configuration using adapter-based serialization."""
564
607
  config_path = self.get_config_path()
565
608
  if not config_path:
566
609
  return False
@@ -578,14 +621,15 @@ class GeminiHostStrategy(MCPHostStrategy):
578
621
  except Exception:
579
622
  pass
580
623
 
581
- # Convert MCPServerConfig objects to dict (REPLACE, don't merge)
624
+ # Use adapter for serialization (includes validation and field filtering)
625
+ adapter = get_adapter(self.get_adapter_host_name())
582
626
  servers_dict = {}
583
627
  for name, server_config in config.servers.items():
584
- servers_dict[name] = server_config.model_dump(exclude_none=True)
628
+ servers_dict[name] = adapter.serialize(server_config)
585
629
 
586
630
  # Update configuration with new servers (preserves non-MCP settings)
587
631
  existing_config[self.get_config_key()] = servers_dict
588
-
632
+
589
633
  # Write atomically with enhanced error handling
590
634
  temp_path = config_path.with_suffix('.tmp')
591
635
  try:
@@ -605,7 +649,7 @@ class GeminiHostStrategy(MCPHostStrategy):
605
649
  temp_path.unlink()
606
650
  logger.error(f"JSON serialization/verification failed: {json_error}")
607
651
  raise
608
-
652
+
609
653
  except Exception as e:
610
654
  logger.error(f"Failed to write Gemini configuration: {e}")
611
655
  return False
@@ -623,6 +667,10 @@ class CodexHostStrategy(MCPHostStrategy):
623
667
  self.config_format = "toml"
624
668
  self._preserved_features = {} # Preserve [features] section
625
669
 
670
+ def get_adapter_host_name(self) -> str:
671
+ """Return the adapter host name for Codex."""
672
+ return "codex"
673
+
626
674
  def get_config_path(self) -> Optional[Path]:
627
675
  """Get Codex configuration path."""
628
676
  return Path.home() / ".codex" / "config.toml"
@@ -673,7 +721,7 @@ class CodexHostStrategy(MCPHostStrategy):
673
721
  return HostConfiguration(servers={})
674
722
 
675
723
  def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
676
- """Write Codex TOML configuration file with backup support."""
724
+ """Write Codex TOML configuration file with backup support using adapter-based serialization."""
677
725
  config_path = self.get_config_path()
678
726
  if not config_path:
679
727
  return False
@@ -694,10 +742,13 @@ class CodexHostStrategy(MCPHostStrategy):
694
742
  if 'features' in existing_data:
695
743
  self._preserved_features = existing_data['features']
696
744
 
697
- # Convert servers to TOML structure
745
+ # Use adapter for serialization (includes validation and field filtering)
746
+ adapter = get_adapter(self.get_adapter_host_name())
698
747
  servers_data = {}
699
748
  for name, server_config in config.servers.items():
700
- servers_data[name] = self._to_toml_server(server_config)
749
+ # Adapter serializes and filters fields, then apply TOML-specific transforms
750
+ serialized = adapter.serialize(server_config)
751
+ servers_data[name] = self._to_toml_server_from_dict(serialized)
701
752
 
702
753
  # Build final TOML structure
703
754
  final_data = {}
@@ -778,3 +829,20 @@ class CodexHostStrategy(MCPHostStrategy):
778
829
  data['http_headers'] = data.pop('headers')
779
830
 
780
831
  return data
832
+
833
+ def _to_toml_server_from_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
834
+ """Apply TOML-specific transformations to an already-serialized dict.
835
+
836
+ This is used after adapter serialization to apply Codex-specific field mappings.
837
+ Maps universal 'headers' field back to Codex-specific 'http_headers'.
838
+ """
839
+ result = dict(data)
840
+
841
+ # Remove 'name' field as it's the table key in TOML
842
+ result.pop('name', None)
843
+
844
+ # Map universal 'headers' to Codex 'http_headers' for TOML
845
+ if 'headers' in result:
846
+ result['http_headers'] = result.pop('headers')
847
+
848
+ return result
@@ -73,7 +73,7 @@ if __name__ == \"__main__\":
73
73
  """
74
74
 
75
75
 
76
- def generate_metadata_json(package_name: str, description: str = ""):
76
+ def generate_metadata_json(package_name: str, description: str = "") -> dict:
77
77
  """Generate the metadata JSON content for a template package.
78
78
 
79
79
  Args:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hatch-xclam
3
- Version: 0.7.1.dev3
3
+ Version: 0.8.0.dev1
4
4
  Summary: Package manager for the Cracking Shells ecosystem
5
5
  Author: Cracking Shells Team
6
6
  Project-URL: Homepage, https://github.com/CrackingShells/Hatch
@@ -22,7 +22,8 @@ Provides-Extra: docs
22
22
  Requires-Dist: mkdocs>=1.4.0; extra == "docs"
23
23
  Requires-Dist: mkdocstrings[python]>=0.20.0; extra == "docs"
24
24
  Provides-Extra: dev
25
- Requires-Dist: wobble>=0.2.0; extra == "dev"
25
+ Requires-Dist: cs-wobble>=0.2.0; extra == "dev"
26
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
26
27
  Dynamic: license-file
27
28
 
28
29
  # Hatch