pomera-ai-commander 1.2.11 → 1.3.1

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  A desktop text "workbench" + MCP server: clean, transform, extract, and analyze text fast—manually in a GUI or programmatically from AI assistants (Cursor / Claude Desktop / MCP clients).
10
10
 
11
- > Hook: Stop pasting text into 10 random websites. Pomera gives you one place (GUI + MCP) to do the 90% text ops you repeat every week.
11
+ > Stop pasting text into 10 random websites. Pomera (GUI + MCP) - do web searches with MCP and save your work as Pomera Notes in case of text corruption in IDE! Your search API keys are stored encrypted in local database instead of JSON config file.
12
12
 
13
13
  [Download latest release](https://github.com/matbanik/Pomera-AI-Commander/releases) · Docs: [Tools](docs/TOOLS_DOCUMENTATION.md) · [MCP Guide](docs/MCP_SERVER_GUIDE.md) · [Troubleshooting](docs/TROUBLESHOOTING.md)
14
14
 
package/mcp.json CHANGED
@@ -1,51 +1,51 @@
1
1
  {
2
- "name": "pomera-ai-commander",
3
- "version": "1.2.11",
4
- "description": "Text processing toolkit with 22 MCP tools including case transformation, encoding, hashing, text analysis, and notes management for AI assistants.",
5
- "homepage": "https://github.com/matbanik/Pomera-AI-Commander",
6
- "repository": "https://github.com/matbanik/Pomera-AI-Commander",
7
- "license": "MIT",
8
- "author": "matbanik",
9
- "entrypoints": {
10
- "stdio": "python pomera_mcp_server.py"
11
- },
12
- "capabilities": {
13
- "tools": true,
14
- "resources": false,
15
- "prompts": false
16
- },
17
- "tools": [
18
- "pomera_case_transform",
19
- "pomera_encode",
20
- "pomera_line_tools",
21
- "pomera_whitespace",
22
- "pomera_string_escape",
23
- "pomera_sort",
24
- "pomera_text_stats",
25
- "pomera_json_xml",
26
- "pomera_url_parse",
27
- "pomera_text_wrap",
28
- "pomera_timestamp",
29
- "pomera_extract",
30
- "pomera_markdown",
31
- "pomera_translator",
32
- "pomera_cron",
33
- "pomera_word_frequency",
34
- "pomera_column_tools",
35
- "pomera_generators",
36
- "pomera_notes",
37
- "pomera_email_header_analyzer",
38
- "pomera_html",
39
- "pomera_list_compare"
40
- ],
41
- "keywords": [
42
- "mcp",
43
- "text-processing",
44
- "ai-tools",
45
- "pomera",
46
- "text-manipulation",
47
- "encoding",
48
- "hashing",
49
- "notes"
50
- ]
51
- }
2
+ "name": "pomera-ai-commander",
3
+ "version": "1.2.0",
4
+ "description": "Text processing toolkit with 22 MCP tools including case transformation, encoding, hashing, text analysis, and notes management for AI assistants.",
5
+ "homepage": "https://github.com/matbanik/Pomera-AI-Commander",
6
+ "repository": "https://github.com/matbanik/Pomera-AI-Commander",
7
+ "license": "MIT",
8
+ "author": "matbanik",
9
+ "entrypoints": {
10
+ "stdio": "python pomera_mcp_server.py"
11
+ },
12
+ "capabilities": {
13
+ "tools": true,
14
+ "resources": false,
15
+ "prompts": false
16
+ },
17
+ "tools": [
18
+ "pomera_case_transform",
19
+ "pomera_encode",
20
+ "pomera_line_tools",
21
+ "pomera_whitespace",
22
+ "pomera_string_escape",
23
+ "pomera_sort",
24
+ "pomera_text_stats",
25
+ "pomera_json_xml",
26
+ "pomera_url_parse",
27
+ "pomera_text_wrap",
28
+ "pomera_timestamp",
29
+ "pomera_extract",
30
+ "pomera_markdown",
31
+ "pomera_translator",
32
+ "pomera_cron",
33
+ "pomera_word_frequency",
34
+ "pomera_column_tools",
35
+ "pomera_generators",
36
+ "pomera_notes",
37
+ "pomera_email_header_analyzer",
38
+ "pomera_html",
39
+ "pomera_list_compare"
40
+ ],
41
+ "keywords": [
42
+ "mcp",
43
+ "text-processing",
44
+ "ai-tools",
45
+ "pomera",
46
+ "text-manipulation",
47
+ "encoding",
48
+ "hashing",
49
+ "notes"
50
+ ]
51
+ }
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
1
  {
2
- "name": "pomera-ai-commander",
3
- "mcpName": "io.github.matbanik/pomera",
4
- "version": "1.2.11",
5
- "description": "Text processing toolkit with 22 MCP tools for AI assistants - case transformation, encoding, hashing, text analysis, and notes management",
6
- "main": "pomera_mcp_server.py",
7
- "bin": {
8
- "pomera-ai-commander": "./bin/pomera-ai-commander.js",
9
- "pomera-mcp": "./bin/pomera-ai-commander.js",
10
- "pomera": "./bin/pomera.js",
11
- "pomera-create-shortcut": "./bin/pomera-create-shortcut.js"
12
- },
13
- "scripts": {
14
- "start": "python pomera_mcp_server.py",
15
- "mcp": "python pomera_mcp_server.py",
16
- "list-tools": "python pomera_mcp_server.py --list-tools",
17
- "test": "python -m pytest",
18
- "create-shortcut": "node scripts/postinstall.js",
19
- "postinstall": "node scripts/postinstall.js"
20
- },
21
- "keywords": [
22
- "mcp",
23
- "model-context-protocol",
24
- "text-processing",
25
- "ai-tools",
26
- "pomera",
27
- "text-manipulation",
28
- "encoding",
29
- "hashing",
30
- "notes",
31
- "cursor",
32
- "claude-desktop"
33
- ],
34
- "author": "matbanik",
35
- "license": "MIT",
36
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/matbanik/Pomera-AI-Commander.git"
39
- },
40
- "bugs": {
41
- "url": "https://github.com/matbanik/Pomera-AI-Commander/issues"
42
- },
43
- "homepage": "https://github.com/matbanik/Pomera-AI-Commander#readme",
44
- "engines": {
45
- "node": ">=14.0.0"
46
- },
47
- "os": [
48
- "darwin",
49
- "linux",
50
- "win32"
51
- ],
52
- "files": [
53
- "bin/",
54
- "core/",
55
- "tools/",
56
- "scripts/",
57
- "resources/",
58
- "pomera_mcp_server.py",
59
- "pomera.py",
60
- "create_shortcut.py",
61
- "migrate_data.py",
62
- "mcp.json",
63
- "README.md",
64
- "LICENSE",
65
- "requirements.txt"
66
- ],
67
- "dependencies": {
68
- "@upstash/c7score": "^3.0.5"
69
- }
2
+ "name": "pomera-ai-commander",
3
+ "mcpName": "io.github.matbanik/pomera",
4
+ "version": "1.3.1",
5
+ "description": "Text processing toolkit with 22 MCP tools for AI assistants - case transformation, encoding, hashing, text analysis, and notes management",
6
+ "main": "pomera_mcp_server.py",
7
+ "bin": {
8
+ "pomera-ai-commander": "./bin/pomera-ai-commander.js",
9
+ "pomera-mcp": "./bin/pomera-ai-commander.js",
10
+ "pomera": "./bin/pomera.js",
11
+ "pomera-create-shortcut": "./bin/pomera-create-shortcut.js"
12
+ },
13
+ "scripts": {
14
+ "start": "python pomera_mcp_server.py",
15
+ "mcp": "python pomera_mcp_server.py",
16
+ "list-tools": "python pomera_mcp_server.py --list-tools",
17
+ "test": "python -m pytest",
18
+ "create-shortcut": "node scripts/postinstall.js",
19
+ "postinstall": "node scripts/postinstall.js"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "text-processing",
25
+ "ai-tools",
26
+ "pomera",
27
+ "text-manipulation",
28
+ "encoding",
29
+ "hashing",
30
+ "notes",
31
+ "cursor",
32
+ "claude-desktop"
33
+ ],
34
+ "author": "matbanik",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/matbanik/Pomera-AI-Commander.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/matbanik/Pomera-AI-Commander/issues"
42
+ },
43
+ "homepage": "https://github.com/matbanik/Pomera-AI-Commander#readme",
44
+ "engines": {
45
+ "node": ">=14.0.0"
46
+ },
47
+ "os": [
48
+ "darwin",
49
+ "linux",
50
+ "win32"
51
+ ],
52
+ "files": [
53
+ "bin/",
54
+ "core/",
55
+ "tools/",
56
+ "scripts/",
57
+ "resources/",
58
+ "pomera_mcp_server.py",
59
+ "pomera.py",
60
+ "create_shortcut.py",
61
+ "migrate_data.py",
62
+ "mcp.json",
63
+ "README.md",
64
+ "LICENSE",
65
+ "requirements.txt"
66
+ ],
67
+ "dependencies": {
68
+ "@upstash/c7score": "^3.0.5"
69
+ }
70
70
  }
package/pomera.py CHANGED
@@ -5901,13 +5901,115 @@ class PromeraAIApp(tk.Tk):
5901
5901
  # Format selection
5902
5902
  self.url_reader_format_var = tk.StringVar(value=self.settings["tool_settings"].get("URL Reader", {}).get("format", "markdown"))
5903
5903
 
5904
- formats = [("Raw HTML", "html"), ("JSON", "json"), ("Markdown", "markdown")]
5904
+ formats = [("Raw HTML", "html"), ("HTML Extraction", "html_extraction"), ("Markdown", "markdown")]
5905
5905
  for text, value in formats:
5906
5906
  rb = ttk.Radiobutton(options_frame, text=text, variable=self.url_reader_format_var, value=value)
5907
5907
  rb.pack(side=tk.LEFT, padx=10)
5908
5908
 
5909
- # Auto-save on change
5910
- self.url_reader_format_var.trace_add("write", lambda *_: self._save_url_reader_settings())
5909
+ # Auto-save on change and update UI
5910
+ self.url_reader_format_var.trace_add("write", lambda *_: self._on_url_reader_format_change())
5911
+
5912
+ # HTML Extraction settings frame (shows when HTML Extraction is selected)
5913
+ self.url_reader_html_settings_frame = ttk.LabelFrame(main_frame, text="HTML Extraction Settings", padding=5)
5914
+
5915
+ # Get current settings or defaults
5916
+ url_reader_settings = self.settings["tool_settings"].get("URL Reader", {})
5917
+
5918
+ # Import HTML tool settings configuration
5919
+ from tools.html_tool import get_default_settings, get_settings_ui_config
5920
+ default_html_settings = get_default_settings()
5921
+
5922
+ # Ensure URL Reader has HTML extraction settings
5923
+ if "html_extraction" not in url_reader_settings:
5924
+ url_reader_settings["html_extraction"] = default_html_settings.copy()
5925
+
5926
+ # Get UI configuration
5927
+ ui_config = get_settings_ui_config()
5928
+
5929
+ # Store setting variables
5930
+ self.url_reader_html_vars = {}
5931
+
5932
+ # Separate UI elements by type for better layout
5933
+ dropdown_configs = []
5934
+ checkbox_configs = []
5935
+ entry_configs = []
5936
+
5937
+ for setting_key, config in ui_config.items():
5938
+ if config["type"] == "dropdown":
5939
+ dropdown_configs.append((setting_key, config))
5940
+ elif config["type"] == "checkbox":
5941
+ checkbox_configs.append((setting_key, config))
5942
+ elif config["type"] == "entry":
5943
+ entry_configs.append((setting_key, config))
5944
+
5945
+ # Create a single row for dropdown and entry elements
5946
+ if dropdown_configs or entry_configs:
5947
+ controls_frame = ttk.Frame(self.url_reader_html_settings_frame)
5948
+ controls_frame.pack(fill=tk.X, pady=5)
5949
+
5950
+ # Create dropdown elements (left side)
5951
+ for setting_key, config in dropdown_configs:
5952
+ ttk.Label(controls_frame, text=config["label"]).pack(side=tk.LEFT, padx=(0, 5))
5953
+
5954
+ current_value = url_reader_settings["html_extraction"].get(setting_key, config["default"])
5955
+ var = tk.StringVar(value=current_value)
5956
+ self.url_reader_html_vars[setting_key] = var
5957
+
5958
+ combo = ttk.Combobox(controls_frame, textvariable=var, state="readonly", width=20)
5959
+ combo['values'] = [option[0] for option in config["options"]]
5960
+
5961
+ # Set the display value based on the internal value
5962
+ for option_display, option_value in config["options"]:
5963
+ if option_value == current_value:
5964
+ var.set(option_display)
5965
+ break
5966
+
5967
+ combo.pack(side=tk.LEFT, padx=(0, 20))
5968
+
5969
+ # Set up change callback
5970
+ var.trace('w', lambda *args, key=setting_key: self._on_url_reader_html_setting_change(key))
5971
+
5972
+ # Create entry elements (right side of same row)
5973
+ for setting_key, config in entry_configs:
5974
+ ttk.Label(controls_frame, text=config["label"]).pack(side=tk.LEFT, padx=(0, 5))
5975
+
5976
+ current_value = url_reader_settings["html_extraction"].get(setting_key, config["default"])
5977
+ var = tk.StringVar(value=current_value)
5978
+ self.url_reader_html_vars[setting_key] = var
5979
+
5980
+ entry = ttk.Entry(controls_frame, textvariable=var, width=15)
5981
+ entry.pack(side=tk.LEFT)
5982
+
5983
+ # Set up change callback
5984
+ var.trace('w', lambda *args, key=setting_key: self._on_url_reader_html_setting_change(key))
5985
+
5986
+ # Create checkbox elements in 3 columns
5987
+ if checkbox_configs:
5988
+ checkbox_frame = ttk.Frame(self.url_reader_html_settings_frame)
5989
+ checkbox_frame.pack(fill=tk.X, pady=5)
5990
+
5991
+ # Configure 3 columns with equal weight
5992
+ checkbox_frame.grid_columnconfigure(0, weight=1)
5993
+ checkbox_frame.grid_columnconfigure(1, weight=1)
5994
+ checkbox_frame.grid_columnconfigure(2, weight=1)
5995
+
5996
+ # Distribute checkboxes across 3 columns
5997
+ for i, (setting_key, config) in enumerate(checkbox_configs):
5998
+ column = i % 3
5999
+ row = i // 3
6000
+
6001
+ current_value = url_reader_settings["html_extraction"].get(setting_key, config["default"])
6002
+ var = tk.BooleanVar(value=current_value)
6003
+ self.url_reader_html_vars[setting_key] = var
6004
+
6005
+ check = ttk.Checkbutton(checkbox_frame, text=config["label"], variable=var)
6006
+ check.grid(row=row, column=column, sticky="w", padx=5, pady=2)
6007
+
6008
+ # Set up change callback
6009
+ var.trace('w', lambda *args, key=setting_key: self._on_url_reader_html_setting_change(key))
6010
+
6011
+ # Show/hide HTML extraction settings based on current format
6012
+ self._update_url_reader_html_settings_visibility()
5911
6013
 
5912
6014
  # Action row
5913
6015
  action_frame = ttk.Frame(main_frame)
@@ -5925,6 +6027,58 @@ class PromeraAIApp(tk.Tk):
5925
6027
  self.url_fetch_in_progress = False
5926
6028
  self.url_fetch_thread = None
5927
6029
 
6030
+ def _on_url_reader_format_change(self):
6031
+ """Handle URL Reader format change."""
6032
+ self._save_url_reader_settings()
6033
+ self._update_url_reader_html_settings_visibility()
6034
+
6035
+ def _update_url_reader_html_settings_visibility(self):
6036
+ """Show or hide HTML extraction settings based on selected format."""
6037
+ if not hasattr(self, 'url_reader_html_settings_frame'):
6038
+ return
6039
+
6040
+ format_value = self.url_reader_format_var.get()
6041
+ if format_value == "html_extraction":
6042
+ self.url_reader_html_settings_frame.pack(fill=tk.X, padx=5, pady=5, before=self.url_fetch_btn.master)
6043
+ else:
6044
+ self.url_reader_html_settings_frame.pack_forget()
6045
+
6046
+ def _on_url_reader_html_setting_change(self, setting_key):
6047
+ """Handle URL Reader HTML extraction setting changes."""
6048
+ if not hasattr(self, 'url_reader_html_vars'):
6049
+ return
6050
+
6051
+ # Ensure settings structure exists
6052
+ if "URL Reader" not in self.settings["tool_settings"]:
6053
+ self.settings["tool_settings"]["URL Reader"] = {}
6054
+ if "html_extraction" not in self.settings["tool_settings"]["URL Reader"]:
6055
+ self.settings["tool_settings"]["URL Reader"]["html_extraction"] = {}
6056
+
6057
+ # Get the actual value based on setting type
6058
+ var = self.url_reader_html_vars[setting_key]
6059
+ if isinstance(var, tk.BooleanVar):
6060
+ value = var.get()
6061
+ elif isinstance(var, tk.StringVar):
6062
+ # For dropdown, convert display value to internal value
6063
+ display_value = var.get()
6064
+ from tools.html_tool import get_settings_ui_config
6065
+ ui_config = get_settings_ui_config()
6066
+ if setting_key in ui_config and ui_config[setting_key]["type"] == "dropdown":
6067
+ # Find the internal value for this display value
6068
+ for option_display, option_value in ui_config[setting_key]["options"]:
6069
+ if option_display == display_value:
6070
+ value = option_value
6071
+ break
6072
+ else:
6073
+ value = display_value
6074
+ else:
6075
+ value = display_value
6076
+ else:
6077
+ value = var.get()
6078
+
6079
+ self.settings["tool_settings"]["URL Reader"]["html_extraction"][setting_key] = value
6080
+ self.save_settings()
6081
+
5928
6082
  def _save_url_reader_settings(self):
5929
6083
  """Save URL Reader settings."""
5930
6084
  if "URL Reader" not in self.settings["tool_settings"]:
@@ -5979,10 +6133,16 @@ class PromeraAIApp(tk.Tk):
5979
6133
  if output_format == "html":
5980
6134
  content = reader.fetch_url(url, timeout=30)
5981
6135
  results.append(f"<!-- URL: {url} -->\n{content}")
5982
- elif output_format == "json":
5983
- import json
5984
- content = reader.fetch_url(url, timeout=30)
5985
- results.append(json.dumps({"url": url, "html": content[:5000]}, indent=2))
6136
+ elif output_format == "html_extraction":
6137
+ # Fetch HTML and process with HTML Extraction Tool
6138
+ from tools.html_tool import HTMLExtractionTool
6139
+ html_content = reader.fetch_url(url, timeout=30)
6140
+ html_tool = HTMLExtractionTool()
6141
+
6142
+ # Get HTML extraction settings
6143
+ html_settings = self.settings["tool_settings"].get("URL Reader", {}).get("html_extraction", {})
6144
+ extracted_content = html_tool.process_text(html_content, html_settings)
6145
+ results.append(f"# Content from: {url}\n\n{extracted_content}")
5986
6146
  else: # markdown
5987
6147
  content = reader.fetch_and_convert(url, timeout=30)
5988
6148
  results.append(f"# Content from: {url}\n\n{content}")
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Version validation script for Pomera AI Commander.
4
+ Validates version format and preconditions before release.
5
+
6
+ Usage:
7
+ python tools/validate_version.py <version>
8
+
9
+ Examples:
10
+ python tools/validate_version.py 1.3.0 # Valid
11
+ python tools/validate_version.py 1.3 # Invalid (must be X.Y.Z)
12
+ python tools/validate_version.py 1.3.0.dev0 # Invalid (.dev suffix)
13
+
14
+ Exit codes:
15
+ 0 - All validation passed
16
+ 1 - Validation failed
17
+ """
18
+
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ import urllib.request
23
+ import json
24
+ from typing import Tuple
25
+
26
+
27
+ def validate_version_format(version: str) -> Tuple[bool, str]:
28
+ """Validate version is X.Y.Z format where X, Y, Z are integers."""
29
+ pattern = r'^\d+\.\d+\.\d+$'
30
+ if not re.match(pattern, version):
31
+ return False, f"Invalid version format: {version}. Must be X.Y.Z (e.g., 1.3.0)"
32
+ return True, "Version format valid"
33
+
34
+
35
+ def validate_no_dev_suffix(version: str) -> Tuple[bool, str]:
36
+ """Validate version does not contain .dev suffix."""
37
+ if '.dev' in version.lower():
38
+ return False, f"Version contains .dev suffix: {version}. Release versions must not have .dev"
39
+ return True, "No .dev suffix"
40
+
41
+
42
+ def validate_git_clean() -> Tuple[bool, str]:
43
+ """Validate git working directory is clean."""
44
+ try:
45
+ result = subprocess.run(
46
+ ['git', 'status', '--porcelain'],
47
+ capture_output=True,
48
+ text=True,
49
+ check=True
50
+ )
51
+ if result.stdout.strip():
52
+ return False, f"Git working directory is not clean:\n{result.stdout}"
53
+ return True, "Git working directory clean"
54
+ except subprocess.CalledProcessError as e:
55
+ return False, f"Failed to check git status: {e}"
56
+
57
+
58
+ def check_pypi_version(version: str) -> bool:
59
+ """Check if version exists on PyPI."""
60
+ try:
61
+ url = f"https://pypi.org/pypi/pomera-ai-commander/{version}/json"
62
+ with urllib.request.urlopen(url, timeout=5) as response:
63
+ return response.status == 200
64
+ except urllib.error.HTTPError as e:
65
+ if e.code == 404:
66
+ return False # Version doesn't exist (good!)
67
+ raise
68
+ except Exception:
69
+ # If we can't check, assume it doesn't exist
70
+ return False
71
+
72
+
73
+ def check_npm_version(version: str) -> bool:
74
+ """Check if version exists on npm."""
75
+ try:
76
+ result = subprocess.run(
77
+ ['npm', 'view', f'pomera-ai-commander@{version}', 'version'],
78
+ capture_output=True,
79
+ text=True,
80
+ timeout=10
81
+ )
82
+ # If npm view succeeds, version exists
83
+ return result.returncode == 0
84
+ except Exception:
85
+ # If we can't check, assume it doesn't exist
86
+ return False
87
+
88
+
89
+ def check_git_tag_exists(version: str) -> bool:
90
+ """Check if Git tag exists locally."""
91
+ try:
92
+ tag = f"v{version}"
93
+ result = subprocess.run(
94
+ ['git', 'tag', '-l', tag],
95
+ capture_output=True,
96
+ text=True,
97
+ check=True
98
+ )
99
+ return result.stdout.strip() == tag
100
+ except Exception:
101
+ return False
102
+
103
+
104
+ def check_github_tag_exists(version: str) -> bool:
105
+ """Check if Git tag exists on remote (GitHub)."""
106
+ try:
107
+ tag = f"v{version}"
108
+ result = subprocess.run(
109
+ ['git', 'ls-remote', '--tags', 'origin', tag],
110
+ capture_output=True,
111
+ text=True,
112
+ timeout=10
113
+ )
114
+ return tag in result.stdout
115
+ except Exception:
116
+ return False
117
+
118
+
119
+ def check_github_release_exists(version: str) -> bool:
120
+ """Check if GitHub release exists."""
121
+ try:
122
+ tag = f"v{version}"
123
+ result = subprocess.run(
124
+ ['gh', 'release', 'view', tag],
125
+ capture_output=True,
126
+ text=True,
127
+ timeout=10
128
+ )
129
+ return result.returncode == 0
130
+ except Exception:
131
+ # If gh CLI not found, skip this check
132
+ return False
133
+
134
+
135
+ def validate_version_not_exists(version: str) -> Tuple[bool, str]:
136
+ """Validate version doesn't already exist anywhere."""
137
+ errors = []
138
+
139
+ if check_git_tag_exists(version):
140
+ errors.append(f"Git tag v{version} already exists locally")
141
+
142
+ if check_github_tag_exists(version):
143
+ errors.append(f"Git tag v{version} already exists on GitHub")
144
+
145
+ if check_github_release_exists(version):
146
+ errors.append(f"GitHub release v{version} already exists")
147
+
148
+ if check_pypi_version(version):
149
+ errors.append(f"PyPI already has version {version}")
150
+
151
+ if check_npm_version(version):
152
+ errors.append(f"npm already has version {version}")
153
+
154
+ if errors:
155
+ return False, "Version already published:\n - " + "\n - ".join(errors)
156
+
157
+ return True, "Version available on all platforms"
158
+
159
+
160
+ def main():
161
+ if len(sys.argv) != 2:
162
+ print("Usage: python tools/validate_version.py <version>", file=sys.stderr)
163
+ print("Example: python tools/validate_version.py 1.3.0", file=sys.stderr)
164
+ sys.exit(1)
165
+
166
+ version = sys.argv[1]
167
+
168
+ print(f"Validating version: {version}")
169
+ print("=" * 60)
170
+
171
+ validations = [
172
+ ("Version format (X.Y.Z)", validate_version_format(version)),
173
+ ("No .dev suffix", validate_no_dev_suffix(version)),
174
+ ("Git clean", validate_git_clean()),
175
+ ("Version not published", validate_version_not_exists(version)),
176
+ ]
177
+
178
+ all_passed = True
179
+ for check_name, (passed, message) in validations:
180
+ status = "✓" if passed else "✗"
181
+ print(f"{status} {check_name}: {message}")
182
+ if not passed:
183
+ all_passed = False
184
+
185
+ print("=" * 60)
186
+
187
+ if all_passed:
188
+ print("✓ All validation checks passed!")
189
+ sys.exit(0)
190
+ else:
191
+ print("✗ Validation failed. Fix issues above before releasing.", file=sys.stderr)
192
+ sys.exit(1)
193
+
194
+
195
+ if __name__ == "__main__":
196
+ main()