llm-ide-rules 0.4.0__py3-none-any.whl → 0.6.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.
@@ -1,218 +1,245 @@
1
1
  """Implode command: Bundle rule files into a single instruction file."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
  from typing_extensions import Annotated
6
- import logging
7
5
 
8
6
  import typer
9
- import structlog
10
-
11
- from llm_ide_rules.constants import load_section_globs, header_to_filename, filename_to_header
12
-
13
- logger = structlog.get_logger()
14
-
15
-
16
- def get_ordered_files(file_list, section_globs_keys):
17
- """Order files based on SECTION_GLOBS key order, with unmapped files at the end."""
18
- file_dict = {f.stem: f for f in file_list}
19
- ordered_files = []
20
-
21
- # Add files in SECTION_GLOBS order
22
- for section_name in section_globs_keys:
23
- filename = header_to_filename(section_name)
24
- if filename in file_dict:
25
- ordered_files.append(file_dict[filename])
26
- del file_dict[filename]
27
-
28
- # Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
29
- remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
30
- ordered_files.extend(remaining_files)
31
-
32
- return ordered_files
33
-
34
-
35
- def get_ordered_files_github(file_list, section_globs_keys):
36
- """Order GitHub instruction files based on SECTION_GLOBS key order, with unmapped files at the end.
37
- Handles .instructions suffix by stripping it for ordering purposes."""
38
- # Create dict mapping base filename (without .instructions) to the actual file
39
- file_dict = {}
40
- for f in file_list:
41
- base_stem = f.stem.replace(".instructions", "")
42
- file_dict[base_stem] = f
43
-
44
- ordered_files = []
45
-
46
- # Add files in SECTION_GLOBS order
47
- for section_name in section_globs_keys:
48
- filename = header_to_filename(section_name)
49
- if filename in file_dict:
50
- ordered_files.append(file_dict[filename])
51
- del file_dict[filename]
52
-
53
- # Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
54
- remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
55
- ordered_files.extend(remaining_files)
56
-
57
- return ordered_files
58
-
59
-
60
- def bundle_cursor_rules(rules_dir, output_file, section_globs):
61
- """Bundle Cursor rule files into a single file."""
62
- rule_files = list(Path(rules_dir).glob("*.mdc"))
63
- general = [f for f in rule_files if f.stem == "general"]
64
- others = [f for f in rule_files if f.stem != "general"]
65
-
66
- # Order the non-general files based on section_globs
67
- ordered_others = get_ordered_files(others, section_globs.keys())
68
- ordered = general + ordered_others
69
-
70
- def resolve_header_from_stem(stem):
71
- """Return the canonical header for a given filename stem.
72
-
73
- Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
74
- Fallback to title-casing the filename when not found in section_globs.
75
- """
76
- for section_name in section_globs.keys():
77
- if header_to_filename(section_name) == stem:
78
- return section_name
79
- return filename_to_header(stem)
80
-
81
- with open(output_file, "w") as out:
82
- for rule_file in ordered:
83
- with open(rule_file, "r") as f:
84
- content = f.read().strip()
85
- if not content:
86
- continue
87
- content = strip_yaml_frontmatter(content)
88
- content = strip_header(content)
89
- # Use canonical header names from SECTION_GLOBS when available
90
- header = resolve_header_from_stem(rule_file.stem)
91
- if rule_file.stem != "general":
92
- out.write(f"## {header}\n\n")
93
- out.write(content)
94
- out.write("\n\n")
95
-
96
-
97
- def strip_yaml_frontmatter(text):
98
- """Strip YAML frontmatter from text."""
99
- lines = text.splitlines()
100
- if lines and lines[0].strip() == "---":
101
- # Find the next '---' after the first
102
- for i in range(1, len(lines)):
103
- if lines[i].strip() == "---":
104
- return "\n".join(lines[i + 1 :]).lstrip("\n")
105
- return text
106
-
107
-
108
- def strip_header(text):
109
- """Remove the first markdown header (## Header) from text if present."""
110
- lines = text.splitlines()
111
- if lines and lines[0].startswith("## "):
112
- # Remove the header line and any immediately following empty lines
113
- remaining_lines = lines[1:]
114
- while remaining_lines and not remaining_lines[0].strip():
115
- remaining_lines = remaining_lines[1:]
116
- return "\n".join(remaining_lines)
117
- return text
118
-
119
-
120
- def bundle_github_instructions(instructions_dir, output_file, section_globs):
121
- """Bundle GitHub instruction files into a single file."""
122
- copilot_general = Path(os.getcwd()) / ".github" / "copilot-instructions.md"
123
- instr_files = list(Path(instructions_dir).glob("*.instructions.md"))
124
-
125
- # Order the instruction files based on section_globs
126
- # We need to create a modified version that strips .instructions from stems for ordering
127
- ordered_files = get_ordered_files_github(instr_files, section_globs.keys())
128
-
129
- def resolve_header_from_stem(stem):
130
- """Return the canonical header for a given filename stem.
131
-
132
- Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
133
- Fallback to title-casing the filename when not found in section_globs.
134
- """
135
- for section_name in section_globs.keys():
136
- if header_to_filename(section_name) == stem:
137
- return section_name
138
- return filename_to_header(stem)
139
-
140
- with open(output_file, "w") as out:
141
- # Write general copilot instructions if present
142
- if copilot_general.exists():
143
- content = copilot_general.read_text().strip()
144
- if content:
145
- out.write(content)
146
- out.write("\n\n")
147
- for instr_file in ordered_files:
148
- with open(instr_file, "r") as f:
149
- content = f.read().strip()
150
- if not content:
151
- continue
152
- content = strip_yaml_frontmatter(content)
153
- content = strip_header(content)
154
- # Use canonical header names from SECTION_GLOBS when available
155
- base_stem = instr_file.stem.replace(".instructions", "")
156
- header = resolve_header_from_stem(base_stem)
157
- out.write(f"## {header}\n\n")
158
- out.write(content)
159
- out.write("\n\n")
7
+
8
+ from llm_ide_rules.agents import get_agent
9
+ from llm_ide_rules.constants import load_section_globs
10
+ from llm_ide_rules.log import log
160
11
 
161
12
 
162
13
  def cursor(
163
- output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
164
- verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
165
- config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
166
- ):
167
- """Bundle Cursor rules into a single file."""
168
- if verbose:
169
- logging.basicConfig(level=logging.DEBUG)
170
- structlog.configure(
171
- wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
172
- )
173
-
174
- # Load section globs (with optional custom config)
175
- SECTION_GLOBS = load_section_globs(config)
176
-
177
- rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
178
- output_path = os.path.join(os.getcwd(), output)
179
-
180
- logger.info("Bundling Cursor rules", rules_dir=rules_dir, output_file=output_path, config=config)
181
-
182
- if not Path(rules_dir).exists():
183
- logger.error("Cursor rules directory not found", rules_dir=rules_dir)
14
+ output: Annotated[
15
+ str, typer.Argument(help="Output file for rules")
16
+ ] = "instructions.md",
17
+ config: Annotated[
18
+ str | None,
19
+ typer.Option("--config", "-c", help="Custom configuration file path"),
20
+ ] = None,
21
+ ) -> None:
22
+ """Bundle Cursor rules into instructions.md and commands into commands.md."""
23
+
24
+ section_globs = load_section_globs(config)
25
+ agent = get_agent("cursor")
26
+ cwd = Path.cwd()
27
+
28
+ rules_dir = agent.rules_dir
29
+ if not rules_dir:
30
+ log.error("cursor rules directory not configured")
31
+ raise typer.Exit(1)
32
+
33
+ log.info(
34
+ "bundling cursor rules and commands",
35
+ rules_dir=rules_dir,
36
+ commands_dir=agent.commands_dir,
37
+ config=config,
38
+ )
39
+
40
+ rules_path = cwd / rules_dir
41
+ if not rules_path.exists():
42
+ log.error("cursor rules directory not found", rules_dir=str(rules_path))
43
+ error_msg = f"Cursor rules directory not found: {rules_path}"
44
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
184
45
  raise typer.Exit(1)
185
-
186
- bundle_cursor_rules(rules_dir, output_path, SECTION_GLOBS)
187
- logger.info("Cursor rules bundled successfully", output_file=output_path)
188
- typer.echo(f"Bundled cursor rules into {output}")
46
+
47
+ output_path = cwd / output
48
+ rules_written = agent.bundle_rules(output_path, section_globs)
49
+ if rules_written:
50
+ success_msg = f"Bundled cursor rules into {output}"
51
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
52
+ else:
53
+ output_path.unlink(missing_ok=True)
54
+ log.info("no cursor rules to bundle")
55
+
56
+ commands_output_path = cwd / "commands.md"
57
+ commands_written = agent.bundle_commands(commands_output_path, section_globs)
58
+ if commands_written:
59
+ success_msg = "Bundled cursor commands into commands.md"
60
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
61
+ else:
62
+ commands_output_path.unlink(missing_ok=True)
189
63
 
190
64
 
191
65
  def github(
192
- output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
193
- verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
194
- config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
195
- ):
196
- """Bundle GitHub/Copilot instructions into a single file."""
197
- if verbose:
198
- logging.basicConfig(level=logging.DEBUG)
199
- structlog.configure(
200
- wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
66
+ output: Annotated[
67
+ str, typer.Argument(help="Output file for instructions")
68
+ ] = "instructions.md",
69
+ config: Annotated[
70
+ str | None,
71
+ typer.Option("--config", "-c", help="Custom configuration file path"),
72
+ ] = None,
73
+ ) -> None:
74
+ """Bundle GitHub instructions into instructions.md and prompts into commands.md."""
75
+
76
+ section_globs = load_section_globs(config)
77
+ agent = get_agent("github")
78
+ cwd = Path.cwd()
79
+
80
+ rules_dir = agent.rules_dir
81
+ if not rules_dir:
82
+ log.error("github rules directory not configured")
83
+ raise typer.Exit(1)
84
+
85
+ log.info(
86
+ "bundling github instructions and prompts",
87
+ instructions_dir=rules_dir,
88
+ prompts_dir=agent.commands_dir,
89
+ config=config,
90
+ )
91
+
92
+ rules_path = cwd / rules_dir
93
+ if not rules_path.exists():
94
+ log.error(
95
+ "github instructions directory not found", instructions_dir=str(rules_path)
96
+ )
97
+ error_msg = f"GitHub instructions directory not found: {rules_path}"
98
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
99
+ raise typer.Exit(1)
100
+
101
+ output_path = cwd / output
102
+ instructions_written = agent.bundle_rules(output_path, section_globs)
103
+ if instructions_written:
104
+ success_msg = f"Bundled github instructions into {output}"
105
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
106
+ else:
107
+ output_path.unlink(missing_ok=True)
108
+ log.info("no github instructions to bundle")
109
+
110
+ commands_output_path = cwd / "commands.md"
111
+ prompts_written = agent.bundle_commands(commands_output_path, section_globs)
112
+ if prompts_written:
113
+ success_msg = "Bundled github prompts into commands.md"
114
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
115
+ else:
116
+ commands_output_path.unlink(missing_ok=True)
117
+
118
+
119
+ def claude(
120
+ output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
121
+ config: Annotated[
122
+ str | None,
123
+ typer.Option("--config", "-c", help="Custom configuration file path"),
124
+ ] = None,
125
+ ) -> None:
126
+ """Bundle Claude Code commands into commands.md."""
127
+
128
+ section_globs = load_section_globs(config)
129
+ agent = get_agent("claude")
130
+ cwd = Path.cwd()
131
+
132
+ commands_dir = agent.commands_dir
133
+ if not commands_dir:
134
+ log.error("claude code commands directory not configured")
135
+ raise typer.Exit(1)
136
+
137
+ log.info(
138
+ "bundling claude code commands",
139
+ commands_dir=commands_dir,
140
+ config=config,
141
+ )
142
+
143
+ commands_path = cwd / commands_dir
144
+ if not commands_path.exists():
145
+ log.error(
146
+ "claude code commands directory not found", commands_dir=str(commands_path)
147
+ )
148
+ error_msg = f"Claude Code commands directory not found: {commands_path}"
149
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
150
+ raise typer.Exit(1)
151
+
152
+ output_path = cwd / output
153
+ commands_written = agent.bundle_commands(output_path, section_globs)
154
+ if commands_written:
155
+ success_msg = f"Bundled claude commands into {output}"
156
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
157
+ else:
158
+ output_path.unlink(missing_ok=True)
159
+ log.info("no claude commands to bundle")
160
+
161
+
162
+ def gemini(
163
+ output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
164
+ config: Annotated[
165
+ str | None,
166
+ typer.Option("--config", "-c", help="Custom configuration file path"),
167
+ ] = None,
168
+ ) -> None:
169
+ """Bundle Gemini CLI commands into commands.md."""
170
+
171
+ section_globs = load_section_globs(config)
172
+ agent = get_agent("gemini")
173
+ cwd = Path.cwd()
174
+
175
+ commands_dir = agent.commands_dir
176
+ if not commands_dir:
177
+ log.error("gemini cli commands directory not configured")
178
+ raise typer.Exit(1)
179
+
180
+ log.info(
181
+ "bundling gemini cli commands",
182
+ commands_dir=commands_dir,
183
+ config=config,
184
+ )
185
+
186
+ commands_path = cwd / commands_dir
187
+ if not commands_path.exists():
188
+ log.error(
189
+ "gemini cli commands directory not found", commands_dir=str(commands_path)
190
+ )
191
+ error_msg = f"Gemini CLI commands directory not found: {commands_path}"
192
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
193
+ raise typer.Exit(1)
194
+
195
+ output_path = cwd / output
196
+ commands_written = agent.bundle_commands(output_path, section_globs)
197
+ if commands_written:
198
+ success_msg = f"Bundled gemini commands into {output}"
199
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
200
+ else:
201
+ output_path.unlink(missing_ok=True)
202
+ log.info("no gemini commands to bundle")
203
+
204
+
205
+ def opencode(
206
+ output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
207
+ config: Annotated[
208
+ str | None,
209
+ typer.Option("--config", "-c", help="Custom configuration file path"),
210
+ ] = None,
211
+ ) -> None:
212
+ """Bundle OpenCode commands into commands.md."""
213
+
214
+ section_globs = load_section_globs(config)
215
+ agent = get_agent("opencode")
216
+ cwd = Path.cwd()
217
+
218
+ commands_dir = agent.commands_dir
219
+ if not commands_dir:
220
+ log.error("opencode commands directory not configured")
221
+ raise typer.Exit(1)
222
+
223
+ log.info(
224
+ "bundling opencode commands",
225
+ commands_dir=commands_dir,
226
+ config=config,
227
+ )
228
+
229
+ commands_path = cwd / commands_dir
230
+ if not commands_path.exists():
231
+ log.error(
232
+ "opencode commands directory not found", commands_dir=str(commands_path)
201
233
  )
202
-
203
- # Load section globs (with optional custom config)
204
- SECTION_GLOBS = load_section_globs(config)
205
-
206
- instructions_dir = os.path.join(os.getcwd(), ".github", "instructions")
207
- output_path = os.path.join(os.getcwd(), output)
208
-
209
- logger.info("Bundling GitHub instructions", instructions_dir=instructions_dir, output_file=output_path, config=config)
210
-
211
- if not Path(instructions_dir).exists():
212
- logger.error("GitHub instructions directory not found", instructions_dir=instructions_dir)
234
+ error_msg = f"OpenCode commands directory not found: {commands_path}"
235
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
213
236
  raise typer.Exit(1)
214
-
215
- bundle_github_instructions(instructions_dir, output_path, SECTION_GLOBS)
216
- logger.info("GitHub instructions bundled successfully", output_file=output_path)
217
- typer.echo(f"Bundled github instructions into {output}")
218
237
 
238
+ output_path = cwd / output
239
+ commands_written = agent.bundle_commands(output_path, section_globs)
240
+ if commands_written:
241
+ success_msg = f"Bundled opencode commands into {output}"
242
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
243
+ else:
244
+ output_path.unlink(missing_ok=True)
245
+ log.info("no opencode commands to bundle")
@@ -0,0 +1,119 @@
1
+ """
2
+ MCP configuration management commands.
3
+ """
4
+
5
+ import json5
6
+ import typer
7
+ from pathlib import Path
8
+
9
+ from llm_ide_rules.agents import get_agent, get_all_agents
10
+ from llm_ide_rules.mcp import McpConfig
11
+ from llm_ide_rules.log import log
12
+
13
+ mcp_app = typer.Typer(help="MCP configuration management")
14
+
15
+
16
+ @mcp_app.command()
17
+ def explode(
18
+ input_file: str = typer.Argument("mcp.json", help="Input unified MCP config file"),
19
+ scope: str = typer.Option(
20
+ "project", "--scope", "-s", help="Scope: project, global, or both"
21
+ ),
22
+ agent: str = typer.Option(
23
+ "all",
24
+ "--agent",
25
+ "-a",
26
+ help="Agent: claude, cursor, gemini, opencode, copilot, or all",
27
+ ),
28
+ ) -> None:
29
+ """Convert unified mcp.json to platform-specific configs."""
30
+ input_path = Path(input_file)
31
+ if not input_path.exists():
32
+ log.error("input file not found", file=input_file)
33
+ raise typer.Exit(1)
34
+
35
+ config = McpConfig.model_validate(json5.loads(input_path.read_text()))
36
+
37
+ if agent == "all":
38
+ agents = get_all_agents()
39
+ else:
40
+ agents = [get_agent(agent)]
41
+
42
+ for ag in agents:
43
+ if not ag.mcp_project_path:
44
+ continue
45
+
46
+ mcp_project_path = ag.mcp_project_path
47
+
48
+ servers = {
49
+ name: ag.transform_mcp_server(s) for name, s in config.servers.items()
50
+ }
51
+
52
+ if scope in ("project", "both"):
53
+ project_path = Path.cwd() / mcp_project_path
54
+ ag.write_mcp_config(servers, project_path)
55
+ log.info("wrote project config", agent=ag.name, path=str(project_path))
56
+
57
+ if scope in ("global", "both") and ag.mcp_global_path:
58
+ global_path = Path.home() / ag.mcp_global_path
59
+ ag.write_mcp_config(servers, global_path)
60
+ log.info("wrote global config", agent=ag.name, path=str(global_path))
61
+
62
+
63
+ @mcp_app.command()
64
+ def implode(
65
+ output_file: str = typer.Argument(
66
+ "mcp.json", help="Output unified MCP config file"
67
+ ),
68
+ source: str = typer.Option(
69
+ None, "--source", help="Source agent to read from (e.g., claude, cursor)"
70
+ ),
71
+ scope: str = typer.Option(
72
+ "project", "--scope", "-s", help="Scope: project or global"
73
+ ),
74
+ ) -> None:
75
+ """Merge platform-specific MCP configs into unified mcp.json."""
76
+ if not source:
77
+ log.error("source agent must be specified")
78
+ raise typer.Exit(1)
79
+
80
+ ag = get_agent(source)
81
+
82
+ if scope == "project":
83
+ mcp_project_path = ag.mcp_project_path
84
+ if not mcp_project_path:
85
+ log.error("project config path not defined for agent", agent=source)
86
+ raise typer.Exit(1)
87
+ source_path = Path.cwd() / mcp_project_path
88
+ else:
89
+ mcp_global_path = ag.mcp_global_path
90
+ if not mcp_global_path:
91
+ log.error("global config path not defined for agent", agent=source)
92
+ raise typer.Exit(1)
93
+ source_path = Path.home() / mcp_global_path
94
+
95
+ if not source_path.exists():
96
+ log.error("source config not found", path=str(source_path))
97
+ raise typer.Exit(1)
98
+
99
+ server_configs = ag.read_mcp_config(source_path)
100
+ if not server_configs:
101
+ log.error("no MCP servers found in config", path=str(source_path))
102
+ raise typer.Exit(1)
103
+
104
+ servers = {
105
+ name: ag.reverse_transform_mcp_server(name, config)
106
+ for name, config in server_configs.items()
107
+ }
108
+
109
+ config = McpConfig(servers=servers)
110
+ output_path = Path(output_file)
111
+
112
+ import json
113
+
114
+ output_path.write_text(
115
+ json.dumps(
116
+ config.model_dump(exclude_none=True, exclude_defaults=True), indent=2
117
+ )
118
+ )
119
+ log.info("wrote unified config", path=str(output_path))
@@ -1,36 +1,39 @@
1
1
  """Shared constants for explode and implode functionality."""
2
2
 
3
3
  import json
4
- import os
5
4
  from pathlib import Path
6
5
 
7
- def load_section_globs(custom_config_path: str = None) -> dict:
6
+ VALID_AGENTS = ["cursor", "github", "claude", "gemini", "opencode", "all"]
7
+
8
+
9
+ def load_section_globs(custom_config_path: str | None = None) -> dict[str, str | None]:
8
10
  """Load section globs from JSON config file.
9
-
11
+
10
12
  Args:
11
13
  custom_config_path: Path to custom configuration file to override defaults
12
-
14
+
13
15
  Returns:
14
16
  Dictionary mapping section headers to their file globs or None for prompts
15
17
  """
16
- if custom_config_path and os.path.exists(custom_config_path):
18
+ if custom_config_path and Path(custom_config_path).exists():
17
19
  config_path = Path(custom_config_path)
18
20
  else:
19
- # Load default bundled config
20
21
  config_path = Path(__file__).parent / "sections.json"
21
-
22
- with open(config_path, 'r') as f:
23
- config = json.load(f)
24
-
22
+
23
+ config = json.loads(config_path.read_text())
24
+
25
25
  return config["section_globs"]
26
26
 
27
+
27
28
  # Default section globs - loaded from bundled JSON
28
29
  SECTION_GLOBS = load_section_globs()
29
30
 
30
- def header_to_filename(header):
31
+
32
+ def header_to_filename(header: str) -> str:
31
33
  """Convert a section header to a filename."""
32
- return header.lower().replace(' ', '-')
34
+ return header.lower().replace(" ", "-")
35
+
33
36
 
34
- def filename_to_header(filename):
37
+ def filename_to_header(filename: str) -> str:
35
38
  """Convert a filename back to a section header."""
36
- return filename.replace('-', ' ').title()
39
+ return filename.replace("-", " ").title()
llm_ide_rules/log.py ADDED
@@ -0,0 +1,9 @@
1
+ """Centralized logging configuration using structlog-config.
2
+
3
+ This module configures structlog with opinionated defaults and exports a global logger.
4
+ Import the `log` object from this module to use structured logging throughout the application.
5
+ """
6
+
7
+ from structlog_config import configure_logger
8
+
9
+ log = configure_logger()
@@ -0,0 +1,7 @@
1
+ """
2
+ MCP configuration management.
3
+ """
4
+
5
+ from .models import McpConfig, McpServer
6
+
7
+ __all__ = ["McpServer", "McpConfig"]
@@ -0,0 +1,21 @@
1
+ """
2
+ Pydantic models for unified MCP configuration.
3
+ """
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class McpServer(BaseModel):
9
+ """Unified MCP server definition."""
10
+
11
+ command: str | None = None
12
+ args: list[str] = Field(default_factory=list)
13
+ url: str | None = None
14
+ type: str | None = None
15
+ env: dict[str, str] | None = None
16
+
17
+
18
+ class McpConfig(BaseModel):
19
+ """Unified mcp.json format."""
20
+
21
+ servers: dict[str, McpServer]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "section_globs": {
3
- "Python": "**/*.py",
3
+ "Python": "**/*.py,pyproject.toml",
4
4
  "Python App": "**/*.py",
5
5
  "Pytest Integration Tests": "tests/integration/**/*.py",
6
6
  "Pytest Tests": "tests/**/*.py",
@@ -9,10 +9,9 @@
9
9
  "FastAPI": "app/routes/**/*.py",
10
10
  "React": "**/*.tsx",
11
11
  "React Router": "web/app/routes/**/*.tsx",
12
- "React Router Client Loader": null,
13
12
  "Shell": "**/*.sh",
14
13
  "TypeScript": "**/*.ts,**/*.tsx",
15
- "TypeScript DocString": null,
16
- "Secrets": null
14
+ "Secrets": "env/*.sh,.envrc",
15
+ "Stripe Backend": "manual"
17
16
  }
18
- }
17
+ }