mdinject 0.2.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.
- mdinject/__init__.py +7 -0
- mdinject/__main__.py +270 -0
- mdinject/adapters/__init__.py +15 -0
- mdinject/adapters/content_format.py +356 -0
- mdinject/adapters/export.py +518 -0
- mdinject/adapters/license.py +929 -0
- mdinject/adapters/prompt_storage.py +478 -0
- mdinject/adapters/sqlite_wrapper.py +115 -0
- mdinject/appserver/__init__.py +5 -0
- mdinject/appserver/server.py +820 -0
- mdinject/appserver/token_store.py +24 -0
- mdinject/assets/examples/templates/api-docs.md +56 -0
- mdinject/assets/examples/templates/code-review.md +49 -0
- mdinject/assets/examples/templates/debug-assist.md +47 -0
- mdinject/assets/examples/templates/general-question.md +25 -0
- mdinject/assets/prompt_editor.html +92 -0
- mdinject/assets/terminal.html +107 -0
- mdinject/assets/vendor/all.min.css +9 -0
- mdinject/assets/vendor/all.min.js +6 -0
- mdinject/assets/vendor/easymde.min.css +7 -0
- mdinject/assets/vendor/easymde.min.js +7 -0
- mdinject/assets/vendor/markdown-wysiwyg.css +10 -0
- mdinject/assets/vendor/markdown-wysiwyg.js +6 -0
- mdinject/assets/vendor/milkdown.bundle.css +1 -0
- mdinject/assets/vendor/milkdown.bundle.js +993 -0
- mdinject/assets/vendor/qwebchannel.js +157 -0
- mdinject/assets/vendor/webfonts/fa-regular-400.woff2 +0 -0
- mdinject/assets/vendor/webfonts/fa-solid-900.woff2 +0 -0
- mdinject/assets/vendor/xterm-addon-fit.js +2 -0
- mdinject/assets/vendor/xterm.css +209 -0
- mdinject/assets/vendor/xterm.js +2 -0
- mdinject/config/__init__.py +344 -0
- mdinject/config/examples/cli_profiles.toml +113 -0
- mdinject/config/format_config.py +159 -0
- mdinject/config/loader.py +328 -0
- mdinject/config/profiles.py +200 -0
- mdinject/exporters.py +82 -0
- mdinject/logging/__init__.py +261 -0
- mdinject/mcp/API.md +935 -0
- mdinject/mcp/__init__.py +9 -0
- mdinject/mcp/_app.py +30 -0
- mdinject/mcp/config/__init__.py +7 -0
- mdinject/mcp/config/mdinject_mcp.py +99 -0
- mdinject/mcp/health.py +70 -0
- mdinject/mcp/models.py +316 -0
- mdinject/mcp/server.py +169 -0
- mdinject/mcp/server_core.py +257 -0
- mdinject/mcp/tools/__init__.py +29 -0
- mdinject/mcp/tools/collaboration_tools.py +289 -0
- mdinject/mcp/tools/export_tools.py +170 -0
- mdinject/mcp/tools/format_tools.py +397 -0
- mdinject/mcp/tools/license_tools.py +161 -0
- mdinject/mcp/tools/prompt_tools.py +555 -0
- mdinject/mcp/tools/terminal_tools.py +241 -0
- mdinject/mcp/tools/widget_tools.py +271 -0
- mdinject/profiles.py +204 -0
- mdinject/pty_process.py +424 -0
- mdinject/services/__init__.py +37 -0
- mdinject/services/export_service.py +296 -0
- mdinject/services/format_conversion_service.py +655 -0
- mdinject/services/license_service.py +449 -0
- mdinject/services/orchestrator.py +547 -0
- mdinject/services/prompt_pane_service.py +306 -0
- mdinject/services/prompt_storage_service.py +264 -0
- mdinject/services/terminal_pane_service.py +431 -0
- mdinject/storage/__init__.py +308 -0
- mdinject/storage/async_sqlite_store.py +309 -0
- mdinject/storage/scope.py +145 -0
- mdinject/storage.py +166 -0
- mdinject/template_loader.py +261 -0
- mdinject/templates.py +186 -0
- mdinject/tools/__init__.py +1 -0
- mdinject/tools/macos_app.py +147 -0
- mdinject-0.2.0.dist-info/METADATA +440 -0
- mdinject-0.2.0.dist-info/RECORD +78 -0
- mdinject-0.2.0.dist-info/WHEEL +4 -0
- mdinject-0.2.0.dist-info/entry_points.txt +5 -0
- mdinject-0.2.0.dist-info/licenses/LICENSE +28 -0
mdinject/__init__.py
ADDED
mdinject/__main__.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""MdInject command-line interface with Oneiric integration.
|
|
2
|
+
|
|
3
|
+
This module provides the CLI entry point for mdinject with full Oneiric
|
|
4
|
+
integration including configuration management, structured logging,
|
|
5
|
+
and adapter lifecycle management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from oneiric.core.lifecycle import LifecycleManager
|
|
15
|
+
from oneiric.core.resolution import Candidate, Resolver
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from .adapters import PromptStorageAdapter
|
|
19
|
+
from .config import (
|
|
20
|
+
MdInjectConfig,
|
|
21
|
+
get_config_paths,
|
|
22
|
+
load_app_config,
|
|
23
|
+
)
|
|
24
|
+
from .logging import clear_context, get_logger, scoped_context, setup_logging
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MdInjectApplication:
|
|
28
|
+
"""Main application class with Oneiric integration.
|
|
29
|
+
|
|
30
|
+
This class manages the application lifecycle including configuration,
|
|
31
|
+
logging, adapter registration, and graceful shutdown.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
"""Initialize the application."""
|
|
36
|
+
self._config: MdInjectConfig | None = None
|
|
37
|
+
self._resolver: Resolver | None = None
|
|
38
|
+
self._lifecycle: LifecycleManager | None = None
|
|
39
|
+
# The bound structlog logger doesn't have a precise type that ty
|
|
40
|
+
# can resolve, so type it loosely and narrow at use sites.
|
|
41
|
+
self._logger: Any = None
|
|
42
|
+
self._storage: PromptStorageAdapter | None = None
|
|
43
|
+
|
|
44
|
+
async def startup(self) -> None:
|
|
45
|
+
"""Initialize the application.
|
|
46
|
+
|
|
47
|
+
This method:
|
|
48
|
+
1. Loads configuration
|
|
49
|
+
2. Sets up structured logging
|
|
50
|
+
3. Initializes Oneiric resolver
|
|
51
|
+
4. Registers mdinject adapters
|
|
52
|
+
5. Creates lifecycle manager
|
|
53
|
+
6. Activates storage adapter
|
|
54
|
+
7. Performs health checks
|
|
55
|
+
"""
|
|
56
|
+
# Load configuration
|
|
57
|
+
self._config = load_app_config()
|
|
58
|
+
paths = get_config_paths()
|
|
59
|
+
|
|
60
|
+
# Setup structured logging
|
|
61
|
+
setup_logging(
|
|
62
|
+
level="DEBUG" if self._config.debug else "INFO",
|
|
63
|
+
development=self._config.debug,
|
|
64
|
+
log_file=paths["cache"] / "mdinject.log",
|
|
65
|
+
service_name="mdinject",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self._logger = get_logger("mdinject.app").bind(
|
|
69
|
+
version=__version__,
|
|
70
|
+
environment=self._config.environment,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with scoped_context(phase="startup"):
|
|
74
|
+
self._logger.info("application_startup", config_path=str(paths["config"]))
|
|
75
|
+
|
|
76
|
+
# Initialize Oneiric resolver
|
|
77
|
+
self._resolver = Resolver()
|
|
78
|
+
await self._register_adapters()
|
|
79
|
+
|
|
80
|
+
# Create lifecycle manager
|
|
81
|
+
self._lifecycle = LifecycleManager(resolver=self._resolver)
|
|
82
|
+
|
|
83
|
+
# Activate storage adapter
|
|
84
|
+
await self._activate_storage()
|
|
85
|
+
|
|
86
|
+
# Perform health checks
|
|
87
|
+
await self._health_check()
|
|
88
|
+
|
|
89
|
+
async def _register_adapters(self) -> None:
|
|
90
|
+
"""Register mdinject adapters with the resolver.
|
|
91
|
+
|
|
92
|
+
This method registers all mdinject-specific adapters including
|
|
93
|
+
the prompt storage adapter.
|
|
94
|
+
"""
|
|
95
|
+
assert self._logger is not None
|
|
96
|
+
assert self._resolver is not None
|
|
97
|
+
|
|
98
|
+
self._logger.info("registering_adapters")
|
|
99
|
+
|
|
100
|
+
# Register prompt storage adapter
|
|
101
|
+
# The adapter class itself contains metadata via @property
|
|
102
|
+
storage_adapter = PromptStorageAdapter()
|
|
103
|
+
self._resolver.register(
|
|
104
|
+
Candidate(
|
|
105
|
+
domain="adapter",
|
|
106
|
+
key="prompt_store",
|
|
107
|
+
provider=storage_adapter.metadata.provider,
|
|
108
|
+
factory=PromptStorageAdapter,
|
|
109
|
+
priority=storage_adapter.metadata.priority,
|
|
110
|
+
metadata=storage_adapter.metadata.model_dump(),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
self._logger.debug(
|
|
115
|
+
"adapters_registered",
|
|
116
|
+
storage_provider=storage_adapter.metadata.provider,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def _activate_storage(self) -> None:
|
|
120
|
+
"""Activate the prompt storage adapter.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
RuntimeError: If storage adapter fails to activate or health check fails
|
|
124
|
+
"""
|
|
125
|
+
assert self._config is not None
|
|
126
|
+
assert self._lifecycle is not None
|
|
127
|
+
assert self._logger is not None
|
|
128
|
+
|
|
129
|
+
self._logger.info(
|
|
130
|
+
"activating_storage",
|
|
131
|
+
provider=self._config.storage.adapter_provider,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Create storage adapter instance directly
|
|
136
|
+
# (bypassing resolver activation for direct control)
|
|
137
|
+
self._storage = PromptStorageAdapter()
|
|
138
|
+
await self._storage.init()
|
|
139
|
+
|
|
140
|
+
# Verify storage is healthy
|
|
141
|
+
is_healthy = await self._storage.health()
|
|
142
|
+
if not is_healthy:
|
|
143
|
+
raise RuntimeError("Storage adapter failed health check")
|
|
144
|
+
|
|
145
|
+
self._logger.info(
|
|
146
|
+
"storage_activated",
|
|
147
|
+
database_path=self._config.storage.database_path,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
self._logger.error("storage_activation_failed", error=str(e))
|
|
152
|
+
raise RuntimeError(f"Failed to activate storage: {e}") from e
|
|
153
|
+
|
|
154
|
+
async def _health_check(self) -> None:
|
|
155
|
+
"""Perform health checks on all active components.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
RuntimeError: If any critical component fails health check
|
|
159
|
+
"""
|
|
160
|
+
assert self._logger is not None
|
|
161
|
+
assert self._storage is not None
|
|
162
|
+
|
|
163
|
+
self._logger.info("health_check_start")
|
|
164
|
+
|
|
165
|
+
# Check storage health
|
|
166
|
+
storage_healthy = await self._storage.health()
|
|
167
|
+
self._logger.info(
|
|
168
|
+
"health_check_result",
|
|
169
|
+
storage="healthy" if storage_healthy else "unhealthy",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not storage_healthy:
|
|
173
|
+
raise RuntimeError("Storage health check failed")
|
|
174
|
+
|
|
175
|
+
async def run(self) -> None:
|
|
176
|
+
"""Run the main application logic.
|
|
177
|
+
|
|
178
|
+
For the CLI interface, this simply lists the loaded prompts.
|
|
179
|
+
In a GUI application, this would start the Qt event loop.
|
|
180
|
+
"""
|
|
181
|
+
assert self._logger is not None
|
|
182
|
+
assert self._storage is not None
|
|
183
|
+
assert self._config is not None
|
|
184
|
+
|
|
185
|
+
with scoped_context(phase="run"):
|
|
186
|
+
# List prompts from storage
|
|
187
|
+
prompts = await self._storage.list_prompts()
|
|
188
|
+
|
|
189
|
+
self._logger.info(
|
|
190
|
+
"prompts_loaded",
|
|
191
|
+
count=len(prompts),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Print to console
|
|
195
|
+
print(f"MdInject {__version__} initialized.")
|
|
196
|
+
print(f"Environment: {self._config.environment}")
|
|
197
|
+
print(f"{len(prompts)} prompt(s) loaded.")
|
|
198
|
+
print(f"Database: {self._config.storage.database_path}")
|
|
199
|
+
|
|
200
|
+
# List prompt titles if any exist
|
|
201
|
+
if prompts:
|
|
202
|
+
print("\nPrompts:")
|
|
203
|
+
for prompt in prompts:
|
|
204
|
+
print(f" - {prompt.title}")
|
|
205
|
+
|
|
206
|
+
async def shutdown(self) -> None:
|
|
207
|
+
"""Gracefully shutdown the application.
|
|
208
|
+
|
|
209
|
+
This method:
|
|
210
|
+
1. Cleans up storage adapter
|
|
211
|
+
2. Clears logging context
|
|
212
|
+
"""
|
|
213
|
+
if self._logger:
|
|
214
|
+
self._logger.info("application_shutdown")
|
|
215
|
+
|
|
216
|
+
# Cleanup storage
|
|
217
|
+
if self._storage:
|
|
218
|
+
try:
|
|
219
|
+
await self._storage.cleanup()
|
|
220
|
+
except (RuntimeError, OSError) as e:
|
|
221
|
+
if self._logger:
|
|
222
|
+
self._logger.warning("storage_cleanup_error", error=str(e))
|
|
223
|
+
else:
|
|
224
|
+
print(f"Warning: Storage cleanup error: {e}")
|
|
225
|
+
|
|
226
|
+
# Clear logging context
|
|
227
|
+
clear_context()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def _async_main() -> int:
|
|
231
|
+
"""Async main entry point.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Exit code (0 for success, non-zero for errors)
|
|
235
|
+
"""
|
|
236
|
+
app = MdInjectApplication()
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
await app.startup()
|
|
240
|
+
await app.run()
|
|
241
|
+
await app.shutdown()
|
|
242
|
+
return 0
|
|
243
|
+
except KeyboardInterrupt:
|
|
244
|
+
if app._logger:
|
|
245
|
+
app._logger.info("keyboard_interrupt")
|
|
246
|
+
print("\nInterrupted by user")
|
|
247
|
+
return 130 # Standard exit code for SIGINT
|
|
248
|
+
except (RuntimeError, OSError, ValueError) as e:
|
|
249
|
+
if app._logger:
|
|
250
|
+
app._logger.error("application_error", error=str(e))
|
|
251
|
+
print(f"Error: {e}")
|
|
252
|
+
return 1
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def main() -> None:
|
|
256
|
+
"""Synchronous main entry point.
|
|
257
|
+
|
|
258
|
+
This function is called by the mdinject console script.
|
|
259
|
+
It runs the async main function in an event loop.
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
exit_code = asyncio.run(_async_main())
|
|
263
|
+
sys.exit(exit_code)
|
|
264
|
+
except (RuntimeError, OSError, ValueError) as e:
|
|
265
|
+
print(f"Fatal error: {e}")
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Oneiric adapters for mdinject.
|
|
2
|
+
|
|
3
|
+
This package contains all Oneiric-compatible adapters for mdinject,
|
|
4
|
+
following the adapter pattern with factory registration and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .prompt_storage import PromptStorageAdapter, PromptStorageSettings
|
|
8
|
+
from .sqlite_wrapper import SQLiteDatabaseAdapter, SQLiteDatabaseSettings
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"PromptStorageAdapter",
|
|
12
|
+
"PromptStorageSettings",
|
|
13
|
+
"SQLiteDatabaseAdapter",
|
|
14
|
+
"SQLiteDatabaseSettings",
|
|
15
|
+
]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Content format detection and conversion adapters.
|
|
2
|
+
|
|
3
|
+
This module provides format detection, conversion, and related data
|
|
4
|
+
structures for transforming various content formats to clean markdown.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContentFormat(Enum):
|
|
16
|
+
"""Supported content formats for detection and conversion."""
|
|
17
|
+
|
|
18
|
+
MARKDOWN = "markdown"
|
|
19
|
+
HTML = "html"
|
|
20
|
+
PLAIN_TEXT = "plain_text"
|
|
21
|
+
CODE = "code"
|
|
22
|
+
UNKNOWN = "unknown"
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return self.value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class FormatInfo:
|
|
30
|
+
"""Result of format detection.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
format: The detected content format
|
|
34
|
+
confidence: Detection confidence from 0.0 to 1.0
|
|
35
|
+
language: Detected programming language (for CODE format)
|
|
36
|
+
metadata: Additional format-specific metadata
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
format: ContentFormat
|
|
40
|
+
confidence: float
|
|
41
|
+
language: str | None = None
|
|
42
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
"""Validate confidence is in valid range."""
|
|
46
|
+
if not 0.0 <= self.confidence <= 1.0:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Confidence must be between 0.0 and 1.0, got {self.confidence}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# HTML detection patterns
|
|
53
|
+
HTML_PATTERNS = [
|
|
54
|
+
re.compile(r"<\s*!DOCTYPE\s+html", re.IGNORECASE),
|
|
55
|
+
re.compile(r"<\s*html[^>]*>", re.IGNORECASE),
|
|
56
|
+
re.compile(r"<\s*head[^>]*>", re.IGNORECASE),
|
|
57
|
+
re.compile(r"<\s*body[^>]*>", re.IGNORECASE),
|
|
58
|
+
re.compile(r"<\s*div[^>]*>", re.IGNORECASE),
|
|
59
|
+
re.compile(r"<\s*p[\s>]", re.IGNORECASE),
|
|
60
|
+
re.compile(r"<\s*a\s+href", re.IGNORECASE),
|
|
61
|
+
re.compile(r"<\s*img[^>]*src", re.IGNORECASE),
|
|
62
|
+
re.compile(r"<\s*ul[^>]*>|<\s*ol[^>]*>", re.IGNORECASE),
|
|
63
|
+
re.compile(r"<\s*li[^>]*>", re.IGNORECASE),
|
|
64
|
+
re.compile(r"<\s*h[1-6][^>]*>", re.IGNORECASE),
|
|
65
|
+
re.compile(r"<\s*table[^>]*>", re.IGNORECASE),
|
|
66
|
+
re.compile(r"&[a-z]+;|&#\d+;", re.IGNORECASE), # HTML entities
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Markdown detection patterns
|
|
70
|
+
MARKDOWN_PATTERNS = [
|
|
71
|
+
re.compile(r"^#{1,6}\s+\S", re.MULTILINE), # ATX headings
|
|
72
|
+
re.compile(r"^\S[^\n]*\n[-=]{3,}\s*$", re.MULTILINE), # Setext headings
|
|
73
|
+
re.compile(r"\*\*[^*]+\*\*"), # Bold
|
|
74
|
+
re.compile(r"\*[^*]+\*"), # Italic
|
|
75
|
+
re.compile(r"_[^_]+_"), # Italic underscore
|
|
76
|
+
re.compile(r"`[^`]+`"), # Inline code
|
|
77
|
+
re.compile(r"```\w*[\s\S]*?```"), # Fenced code blocks
|
|
78
|
+
re.compile(r"^\s*[-*+]\s+\S", re.MULTILINE), # Unordered lists
|
|
79
|
+
re.compile(r"^\s*\d+\.\s+\S", re.MULTILINE), # Ordered lists
|
|
80
|
+
re.compile(r"\[[^\]]+\]\([^)]+\)"), # Links
|
|
81
|
+
re.compile(r"!\[[^\]]*\]\([^)]+\)"), # Images
|
|
82
|
+
re.compile(r"^\s*>\s+\S", re.MULTILINE), # Blockquotes
|
|
83
|
+
re.compile(r"^\s*---+\s*$", re.MULTILINE), # Horizontal rules
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# Programming language detection patterns
|
|
87
|
+
LANGUAGE_PATTERNS = {
|
|
88
|
+
"python": [
|
|
89
|
+
re.compile(r"^\s*def\s+\w+\s*\(", re.MULTILINE),
|
|
90
|
+
re.compile(r"^\s*class\s+\w+[:\(]", re.MULTILINE),
|
|
91
|
+
re.compile(r"^\s*import\s+\w+", re.MULTILINE),
|
|
92
|
+
re.compile(r"^\s*from\s+\w+\s+import", re.MULTILINE),
|
|
93
|
+
re.compile(r":\s*#.+$", re.MULTILINE), # Python comments
|
|
94
|
+
re.compile(r"if\s+__name__\s*==\s*['\"]__main__['\"]"),
|
|
95
|
+
],
|
|
96
|
+
"javascript": [
|
|
97
|
+
re.compile(r"^\s*function\s+\w+\s*\(", re.MULTILINE),
|
|
98
|
+
re.compile(r"^\s*const\s+\w+\s*=", re.MULTILINE),
|
|
99
|
+
re.compile(r"^\s*let\s+\w+\s*=", re.MULTILINE),
|
|
100
|
+
re.compile(r"^\s*var\s+\w+\s*=", re.MULTILINE),
|
|
101
|
+
re.compile(r"=>\s*[{(]"),
|
|
102
|
+
re.compile(r"^\s*async\s+function", re.MULTILINE),
|
|
103
|
+
re.compile(r"console\.(log|error|warn)\s*\("),
|
|
104
|
+
],
|
|
105
|
+
"typescript": [
|
|
106
|
+
re.compile(r"^\s*interface\s+\w+", re.MULTILINE),
|
|
107
|
+
re.compile(r"^\s*type\s+\w+\s*=", re.MULTILINE),
|
|
108
|
+
re.compile(r":\s*(string|number|boolean|any)\b"),
|
|
109
|
+
re.compile(r"<[A-Z]\w*>"),
|
|
110
|
+
re.compile(r"^\s*export\s+(interface|type|class|function)", re.MULTILINE),
|
|
111
|
+
],
|
|
112
|
+
"rust": [
|
|
113
|
+
re.compile(r"^\s*fn\s+\w+", re.MULTILINE),
|
|
114
|
+
re.compile(r"^\s*let\s+mut?\s+\w+", re.MULTILINE),
|
|
115
|
+
re.compile(r"^\s*impl\s+\w+", re.MULTILINE),
|
|
116
|
+
re.compile(r"^\s*pub\s+(fn|struct|enum)", re.MULTILINE),
|
|
117
|
+
re.compile(r"^\s*use\s+\w+::", re.MULTILINE),
|
|
118
|
+
re.compile(r"->\s*\w+"),
|
|
119
|
+
],
|
|
120
|
+
"go": [
|
|
121
|
+
re.compile(r"^\s*func\s+\w+", re.MULTILINE),
|
|
122
|
+
re.compile(r"^\s*func\s*\(\w+\s+\*?\w+\)", re.MULTILINE),
|
|
123
|
+
re.compile(r"^\s*package\s+\w+", re.MULTILINE),
|
|
124
|
+
re.compile(r"^\s*import\s*\(", re.MULTILINE),
|
|
125
|
+
re.compile(r":=\s*\w+"),
|
|
126
|
+
],
|
|
127
|
+
"java": [
|
|
128
|
+
re.compile(r"^\s*public\s+class\s+\w+", re.MULTILINE),
|
|
129
|
+
re.compile(r"^\s*private\s+\w+\s+\w+", re.MULTILINE),
|
|
130
|
+
re.compile(r"^\s*public\s+static\s+void\s+main", re.MULTILINE),
|
|
131
|
+
re.compile(r"System\.(out|err)\.print"),
|
|
132
|
+
],
|
|
133
|
+
"bash": [
|
|
134
|
+
re.compile(r"^#!/bin/(ba)?sh", re.MULTILINE),
|
|
135
|
+
re.compile(r"^\s*if\s+\[\[", re.MULTILINE),
|
|
136
|
+
re.compile(r"^\s*for\s+\w+\s+in", re.MULTILINE),
|
|
137
|
+
re.compile(r"\$\{\w+\}"),
|
|
138
|
+
re.compile(r"^\s*echo\s+", re.MULTILINE),
|
|
139
|
+
],
|
|
140
|
+
"sql": [
|
|
141
|
+
re.compile(r"^\s*SELECT\s+", re.IGNORECASE | re.MULTILINE),
|
|
142
|
+
re.compile(r"^\s*INSERT\s+INTO", re.IGNORECASE | re.MULTILINE),
|
|
143
|
+
re.compile(r"^\s*UPDATE\s+\w+\s+SET", re.IGNORECASE | re.MULTILINE),
|
|
144
|
+
re.compile(r"^\s*CREATE\s+TABLE", re.IGNORECASE | re.MULTILINE),
|
|
145
|
+
re.compile(r"^\s*FROM\s+\w+", re.IGNORECASE | re.MULTILINE),
|
|
146
|
+
],
|
|
147
|
+
"yaml": [
|
|
148
|
+
re.compile(r"^\s*\w+:\s*$", re.MULTILINE),
|
|
149
|
+
re.compile(r"^\s*-\s+\w+:", re.MULTILINE),
|
|
150
|
+
re.compile(r"^\s*---\s*$", re.MULTILINE),
|
|
151
|
+
],
|
|
152
|
+
"json": [
|
|
153
|
+
re.compile(r'^\s*\{[\s\S]*"\w+"\s*:\s*', re.MULTILINE),
|
|
154
|
+
re.compile(r"^\s*\[[\s\S]*\]", re.MULTILINE),
|
|
155
|
+
],
|
|
156
|
+
"toml": [
|
|
157
|
+
re.compile(r"^\s*\[\w+\]", re.MULTILINE),
|
|
158
|
+
re.compile(r"^\s*\[\[.+\]\]", re.MULTILINE),
|
|
159
|
+
],
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def detect_format(content: str) -> FormatInfo:
|
|
164
|
+
"""Detect the format of content.
|
|
165
|
+
|
|
166
|
+
This function analyzes content to determine its likely format
|
|
167
|
+
(HTML, Markdown, Code, or plain text) using pattern matching.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
content: The content to analyze
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
FormatInfo with detected format, confidence, and metadata
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
```python
|
|
177
|
+
info = detect_format("<html><body>Hello</body></html>")
|
|
178
|
+
assert info.format == ContentFormat.HTML
|
|
179
|
+
assert info.confidence > 0.8
|
|
180
|
+
```
|
|
181
|
+
"""
|
|
182
|
+
if not content or not content.strip():
|
|
183
|
+
return FormatInfo(format=ContentFormat.PLAIN_TEXT, confidence=1.0)
|
|
184
|
+
|
|
185
|
+
stripped = content.strip()
|
|
186
|
+
|
|
187
|
+
# Check for HTML first (most distinctive patterns)
|
|
188
|
+
html_score = _score_html(stripped)
|
|
189
|
+
if html_score >= 0.7:
|
|
190
|
+
return FormatInfo(
|
|
191
|
+
format=ContentFormat.HTML,
|
|
192
|
+
confidence=html_score,
|
|
193
|
+
metadata={"html_score": html_score},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Check for code with language detection
|
|
197
|
+
code_result = _detect_code(stripped)
|
|
198
|
+
if code_result["is_code"]:
|
|
199
|
+
return FormatInfo(
|
|
200
|
+
format=ContentFormat.CODE,
|
|
201
|
+
confidence=code_result["confidence"],
|
|
202
|
+
language=code_result["language"],
|
|
203
|
+
metadata={"language_scores": code_result["scores"]},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Check for markdown
|
|
207
|
+
md_score = _score_markdown(stripped)
|
|
208
|
+
if md_score >= 0.5:
|
|
209
|
+
return FormatInfo(
|
|
210
|
+
format=ContentFormat.MARKDOWN,
|
|
211
|
+
confidence=md_score,
|
|
212
|
+
metadata={"markdown_score": md_score},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Default to plain text
|
|
216
|
+
# If we have some markdown patterns but low confidence, still might be markdown
|
|
217
|
+
if md_score > 0.2:
|
|
218
|
+
return FormatInfo(
|
|
219
|
+
format=ContentFormat.MARKDOWN,
|
|
220
|
+
confidence=md_score,
|
|
221
|
+
metadata={"markdown_score": md_score, "low_confidence": True},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return FormatInfo(format=ContentFormat.PLAIN_TEXT, confidence=0.8)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _score_html(content: str) -> float:
|
|
228
|
+
"""Score content for HTML likelihood.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
content: Content to analyze
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Score from 0.0 to 1.0 indicating HTML likelihood
|
|
235
|
+
"""
|
|
236
|
+
matches = 0
|
|
237
|
+
for pattern in HTML_PATTERNS:
|
|
238
|
+
if pattern.search(content):
|
|
239
|
+
matches += 1
|
|
240
|
+
|
|
241
|
+
# Normalize score based on number of patterns matched
|
|
242
|
+
# Need at least 2-3 patterns for high confidence
|
|
243
|
+
if matches == 0:
|
|
244
|
+
return 0.0
|
|
245
|
+
if matches == 1:
|
|
246
|
+
return 0.3
|
|
247
|
+
if matches == 2:
|
|
248
|
+
return 0.6
|
|
249
|
+
if matches == 3:
|
|
250
|
+
return 0.8
|
|
251
|
+
|
|
252
|
+
return min(1.0, 0.8 + (matches - 3) * 0.05)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _score_markdown(content: str) -> float:
|
|
256
|
+
"""Score content for Markdown likelihood.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
content: Content to analyze
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Score from 0.0 to 1.0 indicating Markdown likelihood
|
|
263
|
+
"""
|
|
264
|
+
matches = 0
|
|
265
|
+
for pattern in MARKDOWN_PATTERNS:
|
|
266
|
+
if pattern.search(content):
|
|
267
|
+
matches += 1
|
|
268
|
+
|
|
269
|
+
# Normalize score
|
|
270
|
+
if matches == 0:
|
|
271
|
+
return 0.0
|
|
272
|
+
if matches == 1:
|
|
273
|
+
return 0.3
|
|
274
|
+
if matches == 2:
|
|
275
|
+
return 0.5
|
|
276
|
+
if matches == 3:
|
|
277
|
+
return 0.7
|
|
278
|
+
|
|
279
|
+
return min(1.0, 0.7 + (matches - 3) * 0.05)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _detect_code(content: str) -> dict[str, Any]:
|
|
283
|
+
"""Detect if content is code and identify the language.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
content: Content to analyze
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Dict with 'is_code', 'confidence', 'language', and 'scores'
|
|
290
|
+
"""
|
|
291
|
+
# Skip short content
|
|
292
|
+
if len(content) < 20:
|
|
293
|
+
return {"is_code": False, "confidence": 0.0, "language": None, "scores": {}}
|
|
294
|
+
|
|
295
|
+
# Score each language
|
|
296
|
+
scores: dict[str, int] = {}
|
|
297
|
+
for lang, patterns in LANGUAGE_PATTERNS.items():
|
|
298
|
+
lang_score = 0
|
|
299
|
+
for pattern in patterns:
|
|
300
|
+
if pattern.search(content):
|
|
301
|
+
lang_score += 1
|
|
302
|
+
if lang_score > 0:
|
|
303
|
+
scores[lang] = lang_score
|
|
304
|
+
|
|
305
|
+
# Find best match
|
|
306
|
+
if not scores:
|
|
307
|
+
return {"is_code": False, "confidence": 0.0, "language": None, "scores": {}}
|
|
308
|
+
|
|
309
|
+
best_lang = max(scores, key=lambda k: scores[k])
|
|
310
|
+
best_score = scores[best_lang]
|
|
311
|
+
|
|
312
|
+
# Need at least 2 pattern matches for confidence
|
|
313
|
+
if best_score < 2:
|
|
314
|
+
return {"is_code": False, "confidence": 0.0, "language": None, "scores": scores}
|
|
315
|
+
|
|
316
|
+
# Calculate confidence based on number of matches
|
|
317
|
+
num_patterns = len(LANGUAGE_PATTERNS.get(best_lang, []))
|
|
318
|
+
confidence = min(0.95, best_score / max(num_patterns, 1) * 0.8 + 0.3)
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
"is_code": True,
|
|
322
|
+
"confidence": confidence,
|
|
323
|
+
"language": best_lang,
|
|
324
|
+
"scores": scores,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass
|
|
329
|
+
class ProcessedPrompt:
|
|
330
|
+
"""Result of full prompt processing pipeline.
|
|
331
|
+
|
|
332
|
+
Attributes:
|
|
333
|
+
markdown: The final processed markdown content
|
|
334
|
+
original_format: The detected format of the original content
|
|
335
|
+
detected_language: Programming language if content was code
|
|
336
|
+
confidence: Detection confidence
|
|
337
|
+
was_converted: Whether format conversion was applied
|
|
338
|
+
was_sanitized: Whether sanitization was applied
|
|
339
|
+
warnings: List of warning messages
|
|
340
|
+
changes: List of changes made to the content
|
|
341
|
+
secrets_found: List of detected secrets
|
|
342
|
+
blocked: Whether content was blocked from storage
|
|
343
|
+
block_reason: Reason for blocking if blocked=True
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
markdown: str
|
|
347
|
+
original_format: ContentFormat
|
|
348
|
+
detected_language: str | None = None
|
|
349
|
+
confidence: float = 1.0
|
|
350
|
+
was_converted: bool = False
|
|
351
|
+
was_sanitized: bool = False
|
|
352
|
+
warnings: list[str] = field(default_factory=list)
|
|
353
|
+
changes: list[str] = field(default_factory=list)
|
|
354
|
+
secrets_found: list[Any] = field(default_factory=list) # List[SecretMatch]
|
|
355
|
+
blocked: bool = False
|
|
356
|
+
block_reason: str | None = None
|