mdship 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdship/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """mdship — CLI and MCP tool for manipulating markdown files."""
2
+
3
+ __version__ = "0.1.0"
mdship/cli.py ADDED
@@ -0,0 +1,106 @@
1
+ import sys
2
+ from importlib.metadata import version as _pkg_version
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ _VERSION = _pkg_version("mdship")
10
+
11
+ app = typer.Typer(
12
+ help=f"mdship — markdown manipulation tool (version {_VERSION})",
13
+ context_settings={"help_option_names": ["-h", "--help"]},
14
+ )
15
+ err = Console(stderr=True)
16
+
17
+
18
+ def _version_callback(value: bool) -> None:
19
+ if value:
20
+ print(f"mdship {_VERSION}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.callback()
25
+ def _main(
26
+ _: Annotated[
27
+ bool | None,
28
+ typer.Option("--version", "-V", callback=_version_callback, is_eager=True, help="Show version and exit."),
29
+ ] = None,
30
+ ) -> None:
31
+ pass
32
+
33
+
34
+ @app.command()
35
+ def fix_headings(
36
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
37
+ ) -> None:
38
+ """Fix heading levels to ensure consistent hierarchy."""
39
+ if not file.exists():
40
+ err.print(f"[red]Error:[/red] file not found: {file}")
41
+ raise typer.Exit(1)
42
+
43
+ from mdship.markdown import fix_heading_levels
44
+
45
+ content = file.read_text()
46
+ fixed_content = fix_heading_levels(content)
47
+ print(fixed_content, end="")
48
+
49
+
50
+ @app.command()
51
+ def shift_headings(
52
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
53
+ levels: Annotated[int, typer.Option("--levels", "-l", help="Number of levels to shift (positive=lower, negative=higher)")] = 1,
54
+ ) -> None:
55
+ """Shift all headings by the specified number of levels."""
56
+ if not file.exists():
57
+ err.print(f"[red]Error:[/red] file not found: {file}")
58
+ raise typer.Exit(1)
59
+
60
+ from mdship.markdown import shift_heading_levels
61
+
62
+ content = file.read_text()
63
+ shifted_content = shift_heading_levels(content, levels)
64
+ print(shifted_content, end="")
65
+
66
+
67
+ @app.command()
68
+ def add_checksum(
69
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
70
+ algorithm: Annotated[str, typer.Option("--algorithm", "-a", help="Hash algorithm (md5, sha256, sha1)")] = "sha256",
71
+ ) -> None:
72
+ """Add or update checksum in front-matter."""
73
+ if not file.exists():
74
+ err.print(f"[red]Error:[/red] file not found: {file}")
75
+ raise typer.Exit(1)
76
+
77
+ from mdship.markdown import add_content_checksum
78
+
79
+ content = file.read_text()
80
+ updated_content = add_content_checksum(content, algorithm)
81
+ print(updated_content, end="")
82
+
83
+
84
+ @app.command()
85
+ def reflow(
86
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
87
+ width: Annotated[int | None, typer.Option("--width", "-w", help="Line width (0 for one sentence per line)")] = None,
88
+ ) -> None:
89
+ """Reflow paragraphs to specified width or one sentence per line."""
90
+ if not file.exists():
91
+ err.print(f"[red]Error:[/red] file not found: {file}")
92
+ raise typer.Exit(1)
93
+
94
+ from mdship.markdown import reflow_paragraphs
95
+
96
+ content = file.read_text()
97
+ reflowed_content = reflow_paragraphs(content, width)
98
+ print(reflowed_content, end="")
99
+
100
+
101
+ @app.command()
102
+ def mcp() -> None:
103
+ """Start mdship as an MCP server on stdio."""
104
+ from mdship.mcp_server import main as mcp_main
105
+
106
+ mcp_main()
mdship/markdown.py ADDED
@@ -0,0 +1,179 @@
1
+ """Core markdown manipulation functions."""
2
+
3
+ import hashlib
4
+ import re
5
+ from typing import Optional
6
+
7
+
8
+ def fix_heading_levels(content: str) -> str:
9
+ """Fix heading levels to ensure consistent hierarchy.
10
+
11
+ Ensures headings follow proper nesting (no skipping from h1 to h3, etc).
12
+ """
13
+ lines = content.split("\n")
14
+ result = []
15
+ min_level = None
16
+
17
+ for line in lines:
18
+ match = re.match(r"^(#{1,6})\s+(.+)$", line)
19
+ if match:
20
+ level = len(match.group(1))
21
+ text = match.group(2)
22
+
23
+ if min_level is None:
24
+ min_level = level
25
+ else:
26
+ # TODO: Implement logic to adjust heading levels
27
+ pass
28
+
29
+ result.append(line)
30
+ else:
31
+ result.append(line)
32
+
33
+ return "\n".join(result)
34
+
35
+
36
+ def shift_heading_levels(content: str, levels: int) -> str:
37
+ """Shift all headings by the specified number of levels.
38
+
39
+ Positive numbers lower the headings (h1 -> h2), negative numbers raise them (h2 -> h1).
40
+ """
41
+ lines = content.split("\n")
42
+ result = []
43
+
44
+ for line in lines:
45
+ match = re.match(r"^(#{1,6})\s+(.+)$", line)
46
+ if match:
47
+ current_level = len(match.group(1))
48
+ text = match.group(2)
49
+ new_level = max(1, min(6, current_level + levels))
50
+ result.append("#" * new_level + " " + text)
51
+ else:
52
+ result.append(line)
53
+
54
+ return "\n".join(result)
55
+
56
+
57
+ def add_content_checksum(content: str, algorithm: str = "sha256") -> str:
58
+ """Add or update checksum in front-matter.
59
+
60
+ Supports md5, sha1, and sha256 algorithms.
61
+ """
62
+ lines = content.split("\n")
63
+
64
+ if not lines or lines[0] != "---":
65
+ # No YAML front-matter, prepend it
66
+ hash_obj = hashlib.new(algorithm)
67
+ hash_obj.update(content.encode())
68
+ checksum = hash_obj.hexdigest()
69
+ front_matter = f"---\nchecksum: {checksum}\nchecksum_algorithm: {algorithm}\n---\n"
70
+ return front_matter + content
71
+
72
+ # Find the closing --- of front-matter
73
+ end_idx = None
74
+ for i in range(1, len(lines)):
75
+ if lines[i] == "---":
76
+ end_idx = i
77
+ break
78
+
79
+ if end_idx is None:
80
+ # Malformed front-matter, just add at the beginning
81
+ hash_obj = hashlib.new(algorithm)
82
+ hash_obj.update(content.encode())
83
+ checksum = hash_obj.hexdigest()
84
+ front_matter = f"---\nchecksum: {checksum}\nchecksum_algorithm: {algorithm}\n---\n"
85
+ return front_matter + content
86
+
87
+ # Calculate checksum of the content (excluding front-matter)
88
+ content_without_fm = "\n".join(lines[end_idx + 1 :])
89
+ hash_obj = hashlib.new(algorithm)
90
+ hash_obj.update(content_without_fm.encode())
91
+ checksum = hash_obj.hexdigest()
92
+
93
+ # Update or add checksum fields in front-matter
94
+ fm_lines = lines[1:end_idx]
95
+ checksum_line = f"checksum: {checksum}"
96
+ algorithm_line = f"checksum_algorithm: {algorithm}"
97
+
98
+ # Remove existing checksum lines
99
+ fm_lines = [
100
+ line
101
+ for line in fm_lines
102
+ if not line.startswith("checksum:") and not line.startswith("checksum_algorithm:")
103
+ ]
104
+
105
+ # Add new checksum lines
106
+ fm_lines.append(checksum_line)
107
+ fm_lines.append(algorithm_line)
108
+
109
+ result = ["---"] + fm_lines + ["---"] + lines[end_idx + 1 :]
110
+ return "\n".join(result)
111
+
112
+
113
+ def reflow_paragraphs(content: str, width: Optional[int] = None) -> str:
114
+ """Reflow paragraphs to specified width or one sentence per line.
115
+
116
+ If width is None or 0, splits into one sentence per line.
117
+ Otherwise, reflows to the specified width.
118
+ """
119
+ lines = content.split("\n")
120
+ result = []
121
+ in_code_block = False
122
+ current_paragraph = []
123
+
124
+ for line in lines:
125
+ # Track code blocks
126
+ if line.startswith("```"):
127
+ in_code_block = not in_code_block
128
+
129
+ if in_code_block:
130
+ if current_paragraph:
131
+ result.extend(_reflow_paragraph(current_paragraph, width))
132
+ current_paragraph = []
133
+ result.append(line)
134
+ elif line.strip() == "":
135
+ # Empty line marks end of paragraph
136
+ if current_paragraph:
137
+ result.extend(_reflow_paragraph(current_paragraph, width))
138
+ current_paragraph = []
139
+ result.append("")
140
+ elif re.match(r"^[#\-\*]", line) or line.startswith(">"):
141
+ # Heading, list, or blockquote
142
+ if current_paragraph:
143
+ result.extend(_reflow_paragraph(current_paragraph, width))
144
+ current_paragraph = []
145
+ result.append(line)
146
+ else:
147
+ # Regular paragraph line
148
+ current_paragraph.append(line)
149
+
150
+ # Handle remaining paragraph
151
+ if current_paragraph:
152
+ result.extend(_reflow_paragraph(current_paragraph, width))
153
+
154
+ return "\n".join(result)
155
+
156
+
157
+ def _reflow_paragraph(lines: list[str], width: Optional[int] = None) -> list[str]:
158
+ """Reflow a paragraph (list of lines) to the specified width or sentence per line."""
159
+ text = " ".join(line.strip() for line in lines if line.strip())
160
+
161
+ if width is None or width == 0:
162
+ # One sentence per line
163
+ sentences = re.split(r"(?<=[.!?])\s+", text)
164
+ return [s.strip() for s in sentences if s.strip()]
165
+ else:
166
+ # Reflow to width
167
+ result = []
168
+ current_line = ""
169
+ for word in text.split():
170
+ if not current_line:
171
+ current_line = word
172
+ elif len(current_line) + 1 + len(word) <= width:
173
+ current_line += " " + word
174
+ else:
175
+ result.append(current_line)
176
+ current_line = word
177
+ if current_line:
178
+ result.append(current_line)
179
+ return result
mdship/mcp_server.py ADDED
@@ -0,0 +1,133 @@
1
+ """
2
+ MCP server for mdship.
3
+
4
+ Exposes markdown manipulation tools over stdio. Start with:
5
+ mdship mcp
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import sys
13
+
14
+ from mcp.server import Server
15
+ from mcp.types import TextContent, Tool
16
+
17
+
18
+ def main() -> None:
19
+ """Run the MCP server on stdio."""
20
+ server = Server("mdship")
21
+
22
+ @server.list_tools()
23
+ async def list_tools() -> list[Tool]:
24
+ return [
25
+ Tool(
26
+ name="fix_headings",
27
+ description="Fix heading levels to ensure consistent hierarchy",
28
+ inputSchema={
29
+ "type": "object",
30
+ "properties": {
31
+ "content": {
32
+ "type": "string",
33
+ "description": "Markdown content to process",
34
+ }
35
+ },
36
+ "required": ["content"],
37
+ },
38
+ ),
39
+ Tool(
40
+ name="shift_headings",
41
+ description="Shift all headings by the specified number of levels",
42
+ inputSchema={
43
+ "type": "object",
44
+ "properties": {
45
+ "content": {
46
+ "type": "string",
47
+ "description": "Markdown content to process",
48
+ },
49
+ "levels": {
50
+ "type": "integer",
51
+ "description": "Number of levels to shift (positive=lower, negative=higher)",
52
+ "default": 1,
53
+ },
54
+ },
55
+ "required": ["content"],
56
+ },
57
+ ),
58
+ Tool(
59
+ name="add_checksum",
60
+ description="Add or update checksum in front-matter",
61
+ inputSchema={
62
+ "type": "object",
63
+ "properties": {
64
+ "content": {
65
+ "type": "string",
66
+ "description": "Markdown content to process",
67
+ },
68
+ "algorithm": {
69
+ "type": "string",
70
+ "description": "Hash algorithm (md5, sha256, sha1)",
71
+ "default": "sha256",
72
+ },
73
+ },
74
+ "required": ["content"],
75
+ },
76
+ ),
77
+ Tool(
78
+ name="reflow",
79
+ description="Reflow paragraphs to specified width or one sentence per line",
80
+ inputSchema={
81
+ "type": "object",
82
+ "properties": {
83
+ "content": {
84
+ "type": "string",
85
+ "description": "Markdown content to process",
86
+ },
87
+ "width": {
88
+ "type": "integer",
89
+ "description": "Line width (0 or null for one sentence per line)",
90
+ },
91
+ },
92
+ "required": ["content"],
93
+ },
94
+ ),
95
+ ]
96
+
97
+ @server.call_tool()
98
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
99
+ from mdship.markdown import (
100
+ add_content_checksum,
101
+ fix_heading_levels,
102
+ reflow_paragraphs,
103
+ shift_heading_levels,
104
+ )
105
+
106
+ try:
107
+ if name == "fix_headings":
108
+ content = arguments["content"]
109
+ result = fix_heading_levels(content)
110
+ elif name == "shift_headings":
111
+ content = arguments["content"]
112
+ levels = arguments.get("levels", 1)
113
+ result = shift_heading_levels(content, levels)
114
+ elif name == "add_checksum":
115
+ content = arguments["content"]
116
+ algorithm = arguments.get("algorithm", "sha256")
117
+ result = add_content_checksum(content, algorithm)
118
+ elif name == "reflow":
119
+ content = arguments["content"]
120
+ width = arguments.get("width")
121
+ result = reflow_paragraphs(content, width)
122
+ else:
123
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
124
+
125
+ return [TextContent(type="text", text=result)]
126
+ except Exception as e:
127
+ return [TextContent(type="text", text=f"Error: {str(e)}")]
128
+
129
+ async def run():
130
+ async with server:
131
+ await server.wait_for_shutdown()
132
+
133
+ asyncio.run(run())
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdship
3
+ Version: 0.0.0
4
+ Summary: CLI and MCP tool for manipulating markdown files
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: mcp>=1.0
7
+ Requires-Dist: rich>=13
8
+ Requires-Dist: typer>=0.12
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8; extra == 'dev'
11
+ Requires-Dist: ruff>=0.4; extra == 'dev'
@@ -0,0 +1,8 @@
1
+ mdship/__init__.py,sha256=kWJ5Ylr_DHKHbd2myITrih0VfZdWVAKqfgj5C0nf66A,90
2
+ mdship/cli.py,sha256=QVeTombjQe_dvZk58ExEW_HBi5cmoxLVS36f_26Rh_4,3157
3
+ mdship/markdown.py,sha256=HOw2B_NHurpB0s2NtsPKiZLSTJMd7uvZVLc-ygCN8p8,5732
4
+ mdship/mcp_server.py,sha256=xio11hxEw3f5MqQtebNfdPXI0G3nvHCOHPmxy3Mh-Qw,4667
5
+ mdship-0.0.0.dist-info/METADATA,sha256=R4JsKdLR417je5CJwxWwBvOOfIPpw0EcR52W02U1PJA,309
6
+ mdship-0.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ mdship-0.0.0.dist-info/entry_points.txt,sha256=7U-iGlLgwBr3_ccktLo4MOp_9Su38pXErzoun1opP24,42
8
+ mdship-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mdship = mdship.cli:app