mfcli 0.2.1__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 (138) hide show
  1. mfcli/.env.example +72 -0
  2. mfcli/__init__.py +0 -0
  3. mfcli/agents/__init__.py +0 -0
  4. mfcli/agents/controller/__init__.py +0 -0
  5. mfcli/agents/controller/agent.py +19 -0
  6. mfcli/agents/controller/config.yaml +27 -0
  7. mfcli/agents/controller/tools.py +42 -0
  8. mfcli/agents/tools/general.py +118 -0
  9. mfcli/alembic/env.py +61 -0
  10. mfcli/alembic/script.py.mako +28 -0
  11. mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
  12. mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
  13. mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
  14. mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
  15. mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
  16. mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
  17. mfcli/alembic.ini +147 -0
  18. mfcli/cli/__init__.py +0 -0
  19. mfcli/cli/dependencies.py +59 -0
  20. mfcli/cli/main.py +200 -0
  21. mfcli/client/__init__.py +0 -0
  22. mfcli/client/chroma_db.py +184 -0
  23. mfcli/client/docling.py +44 -0
  24. mfcli/client/gemini.py +252 -0
  25. mfcli/client/llama_parse.py +38 -0
  26. mfcli/client/vector_db.py +93 -0
  27. mfcli/constants/__init__.py +0 -0
  28. mfcli/constants/base_enum.py +18 -0
  29. mfcli/constants/directory_names.py +1 -0
  30. mfcli/constants/file_types.py +189 -0
  31. mfcli/constants/gemini.py +1 -0
  32. mfcli/constants/openai.py +6 -0
  33. mfcli/constants/pipeline_run_status.py +3 -0
  34. mfcli/crud/__init__.py +0 -0
  35. mfcli/crud/file.py +42 -0
  36. mfcli/crud/functional_blocks.py +26 -0
  37. mfcli/crud/netlist.py +18 -0
  38. mfcli/crud/pipeline_run.py +17 -0
  39. mfcli/crud/project.py +144 -0
  40. mfcli/digikey/__init__.py +0 -0
  41. mfcli/digikey/digikey.py +105 -0
  42. mfcli/main.py +5 -0
  43. mfcli/mcp/__init__.py +0 -0
  44. mfcli/mcp/configs/cline_mcp_settings.json +11 -0
  45. mfcli/mcp/configs/mfcli.mcp.json +7 -0
  46. mfcli/mcp/mcp_instance.py +6 -0
  47. mfcli/mcp/server.py +37 -0
  48. mfcli/mcp/state_manager.py +51 -0
  49. mfcli/mcp/tools/__init__.py +0 -0
  50. mfcli/mcp/tools/query_knowledgebase.py +108 -0
  51. mfcli/models/__init__.py +10 -0
  52. mfcli/models/base.py +10 -0
  53. mfcli/models/bom.py +71 -0
  54. mfcli/models/datasheet.py +10 -0
  55. mfcli/models/debug_setup.py +64 -0
  56. mfcli/models/file.py +43 -0
  57. mfcli/models/file_docket.py +94 -0
  58. mfcli/models/file_metadata.py +19 -0
  59. mfcli/models/functional_blocks.py +94 -0
  60. mfcli/models/llm_response.py +5 -0
  61. mfcli/models/mcu.py +97 -0
  62. mfcli/models/mcu_errata.py +26 -0
  63. mfcli/models/netlist.py +59 -0
  64. mfcli/models/pdf_parts.py +25 -0
  65. mfcli/models/pipeline_run.py +34 -0
  66. mfcli/models/project.py +27 -0
  67. mfcli/models/project_metadata.py +15 -0
  68. mfcli/pipeline/__init__.py +0 -0
  69. mfcli/pipeline/analysis/__init__.py +0 -0
  70. mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
  71. mfcli/pipeline/analysis/generators/__init__.py +0 -0
  72. mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
  73. mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
  74. mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
  75. mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
  76. mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
  77. mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
  78. mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
  79. mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
  80. mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
  81. mfcli/pipeline/analysis/generators/generator.py +258 -0
  82. mfcli/pipeline/analysis/generators/generator_base.py +18 -0
  83. mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
  84. mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
  85. mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
  86. mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
  87. mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
  88. mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
  89. mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
  90. mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
  91. mfcli/pipeline/classifier.py +93 -0
  92. mfcli/pipeline/data_enricher.py +15 -0
  93. mfcli/pipeline/extractor.py +34 -0
  94. mfcli/pipeline/extractors/__init__.py +0 -0
  95. mfcli/pipeline/extractors/pdf.py +12 -0
  96. mfcli/pipeline/parser.py +120 -0
  97. mfcli/pipeline/parsers/__init__.py +0 -0
  98. mfcli/pipeline/parsers/netlist/__init__.py +0 -0
  99. mfcli/pipeline/parsers/netlist/edif.py +93 -0
  100. mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
  101. mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
  102. mfcli/pipeline/parsers/netlist/pads.py +185 -0
  103. mfcli/pipeline/parsers/netlist/protel.py +166 -0
  104. mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
  105. mfcli/pipeline/pipeline.py +470 -0
  106. mfcli/pipeline/preprocessors/__init__.py +0 -0
  107. mfcli/pipeline/preprocessors/user_guide.py +127 -0
  108. mfcli/pipeline/run_context.py +32 -0
  109. mfcli/pipeline/schema_mapper.py +89 -0
  110. mfcli/pipeline/sub_classifier.py +115 -0
  111. mfcli/utils/__init__.py +0 -0
  112. mfcli/utils/cline_rules.py +256 -0
  113. mfcli/utils/config.py +33 -0
  114. mfcli/utils/configurator.py +324 -0
  115. mfcli/utils/data_cleaner.py +114 -0
  116. mfcli/utils/datasheet_vectorizer.py +283 -0
  117. mfcli/utils/directory_manager.py +116 -0
  118. mfcli/utils/file_upload.py +298 -0
  119. mfcli/utils/files.py +16 -0
  120. mfcli/utils/http_requests.py +54 -0
  121. mfcli/utils/kb_lister.py +89 -0
  122. mfcli/utils/kb_remover.py +173 -0
  123. mfcli/utils/logger.py +28 -0
  124. mfcli/utils/mcp_configurator.py +394 -0
  125. mfcli/utils/migrations.py +18 -0
  126. mfcli/utils/orm.py +43 -0
  127. mfcli/utils/pdf_splitter.py +63 -0
  128. mfcli/utils/pre_uninstall.py +167 -0
  129. mfcli/utils/query_service.py +22 -0
  130. mfcli/utils/system_check.py +306 -0
  131. mfcli/utils/tools.py +98 -0
  132. mfcli/utils/vectorizer.py +28 -0
  133. mfcli-0.2.1.dist-info/METADATA +956 -0
  134. mfcli-0.2.1.dist-info/RECORD +138 -0
  135. mfcli-0.2.1.dist-info/WHEEL +5 -0
  136. mfcli-0.2.1.dist-info/entry_points.txt +4 -0
  137. mfcli-0.2.1.dist-info/licenses/LICENSE +21 -0
  138. mfcli-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,394 @@
1
+ """MCP server auto-configuration for Cline and Claude Code."""
2
+ import json
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import List, Tuple, Optional
9
+
10
+
11
+ def get_mcp_config_paths() -> List[Tuple[str, Path]]:
12
+ """Get potential MCP configuration file paths for different editors."""
13
+ paths = []
14
+ system = platform.system()
15
+
16
+ if system == "Windows":
17
+ appdata = Path(os.environ.get("APPDATA", ""))
18
+ localappdata = Path(os.environ.get("LOCALAPPDATA", ""))
19
+
20
+ # Cline (VS Code extension)
21
+ cline_vscode = appdata / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json"
22
+ if cline_vscode.exists():
23
+ paths.append(("Cline (VS Code)", cline_vscode))
24
+
25
+ # Windsurf/Cline standalone
26
+ home = Path.home()
27
+ cline_standalone = home / ".cline" / "mcp_settings.json"
28
+ if cline_standalone.exists():
29
+ paths.append(("Cline (Standalone)", cline_standalone))
30
+
31
+ # Claude Code (if it exists on Windows - need to verify path)
32
+ # This is a placeholder - actual path may differ
33
+ claude_code = localappdata / "Claude" / "mcp_settings.json"
34
+ if claude_code.exists():
35
+ paths.append(("Claude Code", claude_code))
36
+
37
+ elif system in ["Darwin", "Linux"]: # macOS or Linux
38
+ home = Path.home()
39
+
40
+ # Cline (VS Code extension) - macOS
41
+ if system == "Darwin":
42
+ cline_vscode = home / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json"
43
+ else: # Linux
44
+ cline_vscode = home / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json"
45
+
46
+ if cline_vscode.exists():
47
+ paths.append(("Cline (VS Code)", cline_vscode))
48
+
49
+ # Windsurf/Cline standalone
50
+ cline_standalone = home / ".cline" / "mcp_settings.json"
51
+ if cline_standalone.exists():
52
+ paths.append(("Cline (Standalone)", cline_standalone))
53
+
54
+ # Claude Code
55
+ claude_code = home / ".claude" / "mcp_settings.json"
56
+ if claude_code.exists():
57
+ paths.append(("Claude Code", claude_code))
58
+
59
+ return paths
60
+
61
+
62
+ def backup_config(config_path: Path) -> Path:
63
+ """Create a backup of the configuration file."""
64
+ backup_path = config_path.with_suffix(config_path.suffix + ".backup")
65
+ shutil.copy2(config_path, backup_path)
66
+ return backup_path
67
+
68
+
69
+ def get_mfcli_mcp_config() -> dict:
70
+ """Get the mfcli-mcp server configuration."""
71
+ return {
72
+ "mfcli-mcp": {
73
+ "disabled": False,
74
+ "timeout": 60,
75
+ "type": "stdio",
76
+ "command": "mfcli-mcp"
77
+ }
78
+ }
79
+
80
+
81
+ def update_mcp_config(config_path: Path) -> bool:
82
+ """Update MCP configuration file with mfcli-mcp server."""
83
+ try:
84
+ # Read existing config
85
+ with open(config_path, 'r') as f:
86
+ config = json.load(f)
87
+
88
+ # Ensure mcpServers key exists
89
+ if "mcpServers" not in config:
90
+ config["mcpServers"] = {}
91
+
92
+ # Check if mfcli-mcp already exists
93
+ if "mfcli-mcp" in config["mcpServers"]:
94
+ print(f" ℹ️ mfcli-mcp already configured in this file")
95
+ return True
96
+
97
+ # Add mfcli-mcp configuration
98
+ mfcli_config = get_mfcli_mcp_config()
99
+ config["mcpServers"].update(mfcli_config)
100
+
101
+ # Create backup
102
+ backup_path = backup_config(config_path)
103
+ print(f" 📋 Backup created: {backup_path}")
104
+
105
+ # Write updated config
106
+ with open(config_path, 'w') as f:
107
+ json.dump(config, f, indent=2)
108
+
109
+ return True
110
+
111
+ except Exception as e:
112
+ print(f" ❌ Error updating config: {e}")
113
+ return False
114
+
115
+
116
+ def create_mcp_config(config_path: Path) -> bool:
117
+ """Create a new MCP configuration file with mfcli-mcp server."""
118
+ try:
119
+ # Ensure parent directory exists
120
+ config_path.parent.mkdir(parents=True, exist_ok=True)
121
+
122
+ # Create new config
123
+ config = {
124
+ "mcpServers": get_mfcli_mcp_config()
125
+ }
126
+
127
+ # Write config
128
+ with open(config_path, 'w') as f:
129
+ json.dump(config, f, indent=2)
130
+
131
+ return True
132
+
133
+ except Exception as e:
134
+ print(f" ❌ Error creating config: {e}")
135
+ return False
136
+
137
+
138
+ def verify_mfcli_installation() -> bool:
139
+ """Verify that mfcli-mcp is installed and accessible."""
140
+ try:
141
+ # Check if mfcli-mcp is in PATH
142
+ result = shutil.which("mfcli-mcp")
143
+ if result:
144
+ return True
145
+
146
+ # On Windows, also check Scripts directory
147
+ if platform.system() == "Windows":
148
+ scripts_dir = Path(sys.executable).parent / "Scripts"
149
+ mfcli_mcp = scripts_dir / "mfcli-mcp.exe"
150
+ if mfcli_mcp.exists():
151
+ return True
152
+
153
+ return False
154
+
155
+ except Exception:
156
+ return False
157
+
158
+
159
+ def test_mcp_server() -> bool:
160
+ """Test if MCP server can be started."""
161
+ print(" Testing MCP server...", end=' ')
162
+ sys.stdout.flush()
163
+
164
+ try:
165
+ import subprocess
166
+ # Try to run mfcli-mcp with a timeout
167
+ result = subprocess.run(
168
+ ["mfcli-mcp"],
169
+ capture_output=True,
170
+ timeout=5,
171
+ text=True
172
+ )
173
+ # If it starts without error, that's good enough
174
+ print("✅")
175
+ return True
176
+ except subprocess.TimeoutExpired:
177
+ # Timeout is actually OK - it means the server started
178
+ print("✅")
179
+ return True
180
+ except FileNotFoundError:
181
+ print("❌ mfcli-mcp command not found")
182
+ return False
183
+ except Exception as e:
184
+ print(f"❌ {str(e)[:50]}")
185
+ return False
186
+
187
+
188
+ def check_mcp_configured() -> Tuple[bool, List[Tuple[str, Path]]]:
189
+ """
190
+ Check if MCP server is configured in any detected editor.
191
+
192
+ Returns:
193
+ Tuple of (is_configured, config_paths)
194
+ - is_configured: True if mfcli-mcp is found in at least one config
195
+ - config_paths: List of all detected MCP config paths
196
+ """
197
+ config_paths = get_mcp_config_paths()
198
+
199
+ if not config_paths:
200
+ return False, []
201
+
202
+ # Check if any config has mfcli-mcp configured
203
+ for name, path in config_paths:
204
+ try:
205
+ with open(path, 'r') as f:
206
+ config = json.load(f)
207
+
208
+ if "mcpServers" in config and "mfcli-mcp" in config["mcpServers"]:
209
+ return True, config_paths
210
+ except Exception:
211
+ continue
212
+
213
+ return False, config_paths
214
+
215
+
216
+ def verify_and_prompt_mcp_setup() -> None:
217
+ """
218
+ Verify MCP server configuration and prompt user to set it up if needed.
219
+ Called during 'mfcli init' to ensure MCP is configured.
220
+ """
221
+ print("\n" + "="*70)
222
+ print(" MCP SERVER VERIFICATION")
223
+ print("="*70)
224
+
225
+ # Check if mfcli-mcp is installed
226
+ if not verify_mfcli_installation():
227
+ print("\n ⚠️ mfcli-mcp command not found!")
228
+ print(" MCP server won't be available until installation is complete.")
229
+ print("="*70 + "\n")
230
+ return
231
+
232
+ # Check if MCP is configured
233
+ is_configured, config_paths = check_mcp_configured()
234
+
235
+ if is_configured:
236
+ print("\n ✅ MCP server is already configured!")
237
+ print("="*70 + "\n")
238
+ return
239
+
240
+ # MCP is not configured - prompt user
241
+ if not config_paths:
242
+ print("\n ℹ️ No AI coding assistant configuration files detected.")
243
+ print("\n To use mfcli with AI assistants like Cline or Claude Code,")
244
+ print(" you'll need to configure the MCP server.")
245
+ print("\n Run this command when ready:")
246
+ print(" mfcli setup-mcp")
247
+ print("="*70 + "\n")
248
+ return
249
+
250
+ # Config files exist but mfcli-mcp is not configured
251
+ print(f"\n ⚠️ Found {len(config_paths)} AI coding assistant(s) but mfcli-mcp")
252
+ print(" is not configured yet.")
253
+ print("\n Would you like to configure MCP server now? (y/n): ", end='')
254
+ sys.stdout.flush()
255
+
256
+ try:
257
+ response = input().strip().lower()
258
+ if response in ['y', 'yes']:
259
+ print()
260
+ setup_mcp_servers()
261
+ else:
262
+ print("\n ℹ️ You can configure MCP server later by running:")
263
+ print(" mfcli setup-mcp")
264
+ print("="*70 + "\n")
265
+ except (KeyboardInterrupt, EOFError):
266
+ print("\n\n ℹ️ Skipping MCP configuration. You can run it later with:")
267
+ print(" mfcli setup-mcp")
268
+ print("="*70 + "\n")
269
+
270
+
271
+ def setup_mcp_servers() -> None:
272
+ """Auto-configure MCP servers for detected editors."""
273
+ print("\n" + "="*70)
274
+ print(" MCP SERVER AUTO-CONFIGURATION")
275
+ print("="*70)
276
+ print("\n Detecting installed AI coding assistants...")
277
+
278
+ # Verify mfcli installation
279
+ if not verify_mfcli_installation():
280
+ print("\n ❌ mfcli-mcp command not found!")
281
+ print("\n Please ensure mfcli is installed with:")
282
+ print(" pipx install mfcli")
283
+ print("\n Or if installing from source:")
284
+ print(" pip install .")
285
+ print("="*70 + "\n")
286
+ return
287
+
288
+ # Get configuration paths
289
+ config_paths = get_mcp_config_paths()
290
+
291
+ if not config_paths:
292
+ print("\n ℹ️ No AI coding assistants detected.")
293
+ print("\n Supported editors:")
294
+ print(" - Cline (VS Code extension)")
295
+ print(" - Cline (Standalone)")
296
+ print(" - Claude Code")
297
+ print("\n If you have one of these installed, the configuration file may")
298
+ print(" not exist yet. You can create it manually at:")
299
+
300
+ system = platform.system()
301
+ if system == "Windows":
302
+ print("\n Cline (VS Code):")
303
+ print(" %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json")
304
+ print("\n Cline (Standalone):")
305
+ print(" %USERPROFILE%\\.cline\\mcp_settings.json")
306
+ else:
307
+ print("\n Cline (VS Code):")
308
+ if system == "Darwin":
309
+ print(" ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json")
310
+ else:
311
+ print(" ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json")
312
+ print("\n Cline (Standalone):")
313
+ print(" ~/.cline/mcp_settings.json")
314
+ print("\n Claude Code:")
315
+ print(" ~/.claude/mcp_settings.json")
316
+
317
+ print("\n Then run this command again.")
318
+ print("="*70 + "\n")
319
+ return
320
+
321
+ print(f"\n Found {len(config_paths)} configuration file(s):\n")
322
+
323
+ success_count = 0
324
+ for name, path in config_paths:
325
+ print(f" 📝 {name}")
326
+ print(f" {path}")
327
+
328
+ if update_mcp_config(path):
329
+ print(f" ✅ Successfully configured!\n")
330
+ success_count += 1
331
+ else:
332
+ print(f" ❌ Configuration failed\n")
333
+
334
+ if success_count > 0:
335
+ print("="*70)
336
+ print(f" ✅ Successfully configured {success_count} editor(s)!")
337
+ print("="*70)
338
+ print("\n Next steps:")
339
+ print(" 1. Restart your editor (VS Code, Cline, etc.)")
340
+ print(" 2. The mfcli-mcp server should now be available")
341
+ print(" 3. Try using the 'query_local_rag' tool in your AI assistant")
342
+ print("\n To test the MCP server:")
343
+ print(" mfcli doctor")
344
+ print("\n")
345
+ else:
346
+ print("="*70)
347
+ print(" ⚠️ No configurations were updated.")
348
+ print("="*70 + "\n")
349
+
350
+
351
+ def get_manual_setup_instructions() -> str:
352
+ """Get manual MCP setup instructions."""
353
+ instructions = """
354
+ Manual MCP Setup Instructions
355
+ ==============================
356
+
357
+ If auto-configuration didn't work, you can manually add mfcli-mcp to your
358
+ editor's MCP configuration file.
359
+
360
+ 1. Locate your MCP configuration file:
361
+
362
+ Windows (Cline in VS Code):
363
+ %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json
364
+
365
+ macOS (Cline in VS Code):
366
+ ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
367
+
368
+ Linux (Cline in VS Code):
369
+ ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
370
+
371
+ Cline Standalone:
372
+ ~/.cline/mcp_settings.json
373
+
374
+ Claude Code:
375
+ ~/.claude/mcp_settings.json
376
+
377
+ 2. Add the following configuration to the "mcpServers" section:
378
+
379
+ {
380
+ "mcpServers": {
381
+ "mfcli-mcp": {
382
+ "disabled": false,
383
+ "timeout": 60,
384
+ "type": "stdio",
385
+ "command": "mfcli-mcp"
386
+ }
387
+ }
388
+ }
389
+
390
+ 3. Save the file and restart your editor.
391
+
392
+ 4. The mfcli-mcp server should now be available in your AI assistant.
393
+ """
394
+ return instructions
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+
3
+ from mfcli.utils.files import file_access_check
4
+ from mfcli.utils.logger import get_logger
5
+
6
+ logger = get_logger(__name__)
7
+
8
+
9
+ def run_migrations():
10
+ from alembic.config import Config
11
+ from alembic import command
12
+
13
+ config_file_path = Path(__file__).parent.parent / "alembic.ini"
14
+ if not file_access_check(config_file_path):
15
+ raise RuntimeError(f"Could not find Alembic config file path: {config_file_path}")
16
+
17
+ alembic_cfg = Config(config_file_path)
18
+ command.upgrade(alembic_cfg, "head")
mfcli/utils/orm.py ADDED
@@ -0,0 +1,43 @@
1
+ from functools import lru_cache
2
+
3
+ from sqlalchemy import create_engine, event
4
+ from sqlalchemy.orm import sessionmaker, Session as dbSession
5
+
6
+ from mfcli.models.base import Base
7
+ from mfcli.models.bom import BOM
8
+
9
+ from mfcli.utils.config import get_config
10
+ from mfcli.utils.directory_manager import app_dirs
11
+
12
+ config = get_config()
13
+
14
+
15
+ @lru_cache
16
+ def get_db_url() -> str:
17
+ return f"sqlite:///{app_dirs.app_data_dir / "multifactor.db"}"
18
+
19
+
20
+ engine = create_engine(
21
+ get_db_url(),
22
+ pool_pre_ping=True,
23
+ pool_recycle=1800,
24
+ pool_size=10,
25
+ max_overflow=20,
26
+ pool_timeout=30
27
+ )
28
+
29
+
30
+ @event.listens_for(engine, "connect")
31
+ def enable_sqlite_fk(dbapi_conn, conn_record):
32
+ cursor = dbapi_conn.cursor()
33
+ cursor.execute("PRAGMA foreign_keys=ON;")
34
+ cursor.close()
35
+
36
+
37
+ Session = sessionmaker(bind=engine)
38
+ session = dbSession(engine)
39
+
40
+
41
+ def create_orm():
42
+ engine.connect()
43
+ Base.metadata.create_all(engine)
@@ -0,0 +1,63 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+ from uuid import uuid4
4
+
5
+ import pikepdf
6
+ from io import BytesIO
7
+
8
+ from mfcli.utils.logger import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class PDFSplitter:
14
+ def __init__(self, file_name: str, content: bytes):
15
+ self._name = file_name
16
+ self._content = content
17
+
18
+ @staticmethod
19
+ def _head_page_limit(total_pages: int) -> int:
20
+ if total_pages <= 30:
21
+ return 10
22
+ elif total_pages <= 100:
23
+ return 20
24
+ else:
25
+ return 30
26
+
27
+ def _open_pdf(self) -> pikepdf.Pdf:
28
+ return pikepdf.open(BytesIO(self._content))
29
+
30
+ def split_pdf_head(self) -> Path:
31
+ with self._open_pdf() as src:
32
+ total_pages = len(src.pages)
33
+ page_limit = self._head_page_limit(total_pages)
34
+
35
+ dst = pikepdf.Pdf.new()
36
+ dst.pages.extend(src.pages[:page_limit])
37
+
38
+ output_path = Path(tempfile.mktemp(suffix=".pdf"))
39
+ dst.save(output_path)
40
+
41
+ return output_path
42
+
43
+ def extract_range(
44
+ self,
45
+ start_page: int,
46
+ end_page: int,
47
+ output_folder: Path,
48
+ ) -> Path:
49
+ logger.debug(f"Splitting PDF: {self._name}")
50
+
51
+ output_folder.mkdir(parents=True, exist_ok=True)
52
+
53
+ with self._open_pdf() as src:
54
+ dst = pikepdf.Pdf.new()
55
+ dst.pages.extend(src.pages[start_page:end_page + 1])
56
+
57
+ output_path = output_folder / f"{uuid4().hex}.pdf"
58
+ dst.save(output_path)
59
+
60
+ logger.debug(f"Output PDF part to: {output_path}")
61
+ logger.debug(f"PDF splitter finished: {self._name}")
62
+
63
+ return output_path
@@ -0,0 +1,167 @@
1
+ """
2
+ Pre-uninstall utility for mfcli.
3
+ This module provides cleanup functionality to ensure graceful uninstallation.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import subprocess
9
+ import platform
10
+ from pathlib import Path
11
+ from typing import List, Tuple
12
+
13
+
14
+ def get_running_mfcli_processes() -> List[Tuple[int, str]]:
15
+ """Find all running mfcli and mfcli-mcp processes."""
16
+ processes = []
17
+ system = platform.system()
18
+
19
+ try:
20
+ if system == "Windows":
21
+ # Use tasklist to find processes
22
+ result = subprocess.run(
23
+ ["tasklist", "/FI", "IMAGENAME eq python*", "/FO", "CSV", "/NH"],
24
+ capture_output=True,
25
+ text=True,
26
+ timeout=5
27
+ )
28
+ for line in result.stdout.strip().split('\n'):
29
+ if line:
30
+ parts = line.strip('"').split('","')
31
+ if len(parts) >= 2:
32
+ try:
33
+ pid = int(parts[1])
34
+ # Check if it's running mfcli-mcp
35
+ cmdline_result = subprocess.run(
36
+ ["wmic", "process", "where", f"ProcessId={pid}", "get", "CommandLine", "/value"],
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=5
40
+ )
41
+ if "mfcli-mcp" in cmdline_result.stdout or "mfcli.mcp.server" in cmdline_result.stdout:
42
+ processes.append((pid, "mfcli-mcp server"))
43
+ except (ValueError, subprocess.TimeoutExpired):
44
+ continue
45
+ else:
46
+ # Unix-like systems
47
+ result = subprocess.run(
48
+ ["ps", "aux"],
49
+ capture_output=True,
50
+ text=True,
51
+ timeout=5
52
+ )
53
+ for line in result.stdout.split('\n'):
54
+ if "mfcli-mcp" in line or "mfcli.mcp.server" in line:
55
+ parts = line.split()
56
+ if len(parts) >= 2:
57
+ try:
58
+ pid = int(parts[1])
59
+ processes.append((pid, "mfcli-mcp server"))
60
+ except ValueError:
61
+ continue
62
+ except Exception as e:
63
+ print(f"Warning: Could not check for running processes: {e}", file=sys.stderr)
64
+
65
+ return processes
66
+
67
+
68
+ def check_mcp_server_status():
69
+ """Check if MCP server is running and provide guidance."""
70
+ print("\n" + "="*70)
71
+ print(" MFCLI PRE-UNINSTALL CHECK")
72
+ print("="*70 + "\n")
73
+
74
+ processes = get_running_mfcli_processes()
75
+
76
+ if processes:
77
+ print("⚠️ WARNING: mfcli MCP server processes are currently running!\n")
78
+ print("Running processes:")
79
+ for pid, name in processes:
80
+ print(f" • PID {pid}: {name}")
81
+
82
+ print("\nBefore uninstalling, you should:")
83
+ print(" 1. Stop/restart your IDE (VS Code, Cline, Claude Code)")
84
+ print(" 2. Close any applications using mfcli-mcp")
85
+ print(" 3. Wait a few seconds for processes to fully terminate")
86
+
87
+ if platform.system() == "Windows":
88
+ print("\nTo manually stop these processes, run:")
89
+ for pid, _ in processes:
90
+ print(f" taskkill /F /PID {pid}")
91
+ else:
92
+ print("\nTo manually stop these processes, run:")
93
+ for pid, _ in processes:
94
+ print(f" kill {pid}")
95
+
96
+ return False
97
+ else:
98
+ print("✓ No running mfcli-mcp processes detected\n")
99
+ return True
100
+
101
+
102
+ def cleanup_file_handles():
103
+ """Attempt to close any ChromaDB or database connections."""
104
+ try:
105
+ # Force garbage collection to close any lingering connections
106
+ import gc
107
+ gc.collect()
108
+
109
+ # Try to close ChromaDB connections if imported
110
+ if 'chromadb' in sys.modules:
111
+ print("Closing ChromaDB connections...")
112
+
113
+ print("✓ Cleanup completed\n")
114
+ except Exception as e:
115
+ print(f"Warning during cleanup: {e}\n")
116
+
117
+
118
+ def print_uninstall_instructions():
119
+ """Print step-by-step uninstall instructions."""
120
+ print("\n" + "="*70)
121
+ print(" UNINSTALL INSTRUCTIONS")
122
+ print("="*70 + "\n")
123
+
124
+ print("To safely uninstall mfcli:\n")
125
+
126
+ if platform.system() == "Windows":
127
+ print(" 1. Close VS Code, Cline, or any IDE using mfcli-mcp")
128
+ print(" 2. Wait 5-10 seconds for processes to fully stop")
129
+ print(" 3. Run: pipx uninstall mfcli")
130
+ print("\nIf you still get permission errors:")
131
+ print(" • Run PowerShell as Administrator")
132
+ print(" • Or use: .\\uninstall.ps1")
133
+ print(" • Or manually delete: %USERPROFILE%\\pipx\\venvs\\mfcli")
134
+ else:
135
+ print(" 1. Close your IDE or any applications using mfcli-mcp")
136
+ print(" 2. Wait a few seconds for processes to fully stop")
137
+ print(" 3. Run: pipx uninstall mfcli")
138
+ print("\nIf you still get permission errors:")
139
+ print(" • Use: ./uninstall.sh")
140
+ print(" • Or manually delete: ~/.local/pipx/venvs/mfcli")
141
+
142
+ print("\nNote: Your configuration and data at ~/Multifactor will NOT be deleted.")
143
+ print("To remove that as well, manually delete the Multifactor directory.")
144
+ print()
145
+
146
+
147
+ def run_pre_uninstall_check():
148
+ """Main pre-uninstall check function."""
149
+ cleanup_file_handles()
150
+ server_clear = check_mcp_server_status()
151
+ print_uninstall_instructions()
152
+
153
+ if not server_clear:
154
+ print("\n⚠️ WARNING: Active processes detected. Please stop them before uninstalling.")
155
+ return 1
156
+ else:
157
+ print("\n✓ System is ready for uninstallation.")
158
+ return 0
159
+
160
+
161
+ def main():
162
+ """Entry point for pre-uninstall command."""
163
+ sys.exit(run_pre_uninstall_check())
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()