pomera-ai-commander 1.2.10 → 1.3.0

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
@@ -1,153 +1,131 @@
1
- # MCP Registry
1
+ # Pomera AI Commander (PAC)
2
2
 
3
- The MCP registry provides MCP clients with a list of MCP servers, like an app store for MCP servers.
3
+ <p align="center">
4
+ <img src="resources/icon.png" alt="Pomera - the fluffy Pomeranian mascot" width="128" height="128">
5
+ </p>
4
6
 
5
- [**📤 Publish my MCP server**](docs/modelcontextprotocol-io/quickstart.mdx) | [**⚡️ Live API docs**](https://registry.modelcontextprotocol.io/docs) | [**👀 Ecosystem vision**](docs/design/ecosystem-vision.md) | 📖 **[Full documentation](./docs)**
7
+ [![Download Latest Release](https://img.shields.io/badge/Download-Latest%20Release-blue?style=for-the-badge&logo=github)](https://github.com/matbanik/Pomera-AI-Commander/releases)
6
8
 
7
- ## Development Status
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).
8
10
 
9
- **2025-10-24 update**: The Registry API has entered an **API freeze (v0.1)** 🎉. For the next month or more, the API will remain stable with no breaking changes, allowing integrators to confidently implement support. This freeze applies to v0.1 while development continues on v0. We'll use this period to validate the API in real-world integrations and gather feedback to shape v1 for general availability. Thank you to everyone for your contributions and patience—your involvement has been key to getting us here!
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.
10
12
 
11
- **2025-09-08 update**: The registry has launched in preview 🎉 ([announcement blog post](https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/)). While the system is now more stable, this is still a preview release and breaking changes or data resets may occur. A general availability (GA) release will follow later. We'd love your feedback in [GitHub discussions](https://github.com/modelcontextprotocol/registry/discussions/new?category=ideas) or in the [#registry-dev Discord](https://discord.com/channels/1358869848138059966/1369487942862504016) ([joining details here](https://modelcontextprotocol.io/community/communication)).
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)
12
14
 
13
- Current key maintainers:
14
- - **Adam Jones** (Anthropic) [@domdomegg](https://github.com/domdomegg)
15
- - **Tadas Antanavicius** (PulseMCP) [@tadasant](https://github.com/tadasant)
16
- - **Toby Padilla** (GitHub) [@toby](https://github.com/toby)
17
- - **Radoslav (Rado) Dimitrov** (Stacklok) [@rdimitrov](https://github.com/rdimitrov)
15
+ ---
18
16
 
19
- ## Contributing
17
+ ## 60-second demo (what to expect)
18
+ ![Messy text → clean output → extracted URLs/emails → ready to ship](PAC.gif)
20
19
 
21
- We use multiple channels for collaboration - see [modelcontextprotocol.io/community/communication](https://modelcontextprotocol.io/community/communication).
20
+ **Best-for workflows**
21
+ - Cleaning pasted logs / PDFs (whitespace, wrapping, stats)
22
+ - Extracting emails/URLs/IDs via regex
23
+ - Normalizing case, sorting, columns
24
+ - Hashing/encoding utilities
25
+ - Letting Cursor/Claude call these as MCP tools in a repeatable pipeline
22
26
 
23
- Often (but not always) ideas flow through this pipeline:
27
+ ---
24
28
 
25
- - **[Discord](https://modelcontextprotocol.io/community/communication)** - Real-time community discussions
26
- - **[Discussions](https://github.com/modelcontextprotocol/registry/discussions)** - Propose and discuss product/technical requirements
27
- - **[Issues](https://github.com/modelcontextprotocol/registry/issues)** - Track well-scoped technical work
28
- - **[Pull Requests](https://github.com/modelcontextprotocol/registry/pulls)** - Contribute work towards issues
29
+ ## Prerequisites
29
30
 
30
- ### Quick start:
31
-
32
- #### Pre-requisites
33
-
34
- - **Docker**
35
- - **Go 1.24.x**
36
- - **ko** - Container image builder for Go ([installation instructions](https://ko.build/install/))
37
- - **golangci-lint v2.4.0**
38
-
39
- #### Running the server
31
+ **Python 3.8+** is required for all installation methods.
40
32
 
33
+ ### macOS (Homebrew)
41
34
  ```bash
42
- # Start full development environment
43
- make dev-compose
35
+ # Tkinter support (replace @3.14 with your Python version)
36
+ brew install python-tk@3.14
37
+ pip3 install requests reportlab python-docx
44
38
  ```
45
39
 
46
- This starts the registry at [`localhost:8080`](http://localhost:8080) with PostgreSQL. The database uses ephemeral storage and is reset each time you restart the containers, ensuring a clean state for development and testing.
47
-
48
- **Note:** The registry uses [ko](https://ko.build) to build container images. The `make dev-compose` command automatically builds the registry image with ko and loads it into your local Docker daemon before starting the services.
49
-
50
- By default, the registry seeds from the production API with a filtered subset of servers (to keep startup fast). This ensures your local environment mirrors production behavior and all seed data passes validation. For offline development you can seed from a file without validation with `MCP_REGISTRY_SEED_FROM=data/seed.json MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=false make dev-compose`.
51
-
52
- The setup can be configured with environment variables in [docker-compose.yml](./docker-compose.yml) - see [.env.example](./.env.example) for a reference.
53
-
54
- <details>
55
- <summary>Alternative: Running a pre-built Docker image</summary>
56
-
57
- Pre-built Docker images are automatically published to GitHub Container Registry:
58
-
40
+ ### Ubuntu/Debian
59
41
  ```bash
60
- # Run latest stable release
61
- docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:latest
62
-
63
- # Run latest from main branch (continuous deployment)
64
- docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:main
65
-
66
- # Run specific release version
67
- docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:v1.0.0
68
-
69
- # Run development build from main branch
70
- docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:main-20250906-abc123d
42
+ sudo apt-get install python3-tk
43
+ pip3 install requests reportlab python-docx
71
44
  ```
72
45
 
73
- **Available tags:**
74
- - **Releases**: `latest`, `v1.0.0`, `v1.1.0`, etc.
75
- - **Continuous**: `main` (latest main branch build)
76
- - **Development**: `main-<date>-<sha>` (specific commit builds)
46
+ ### Windows
47
+ Tkinter is included with Python from [python.org](https://python.org).
48
+ ```cmd
49
+ pip install requests reportlab python-docx
50
+ ```
77
51
 
78
- </details>
52
+ > **Note:** For PEP 668 protected environments, use `pip3 install --user` or a virtual environment.
79
53
 
80
- #### Publishing a server
54
+ ---
81
55
 
82
- To publish a server, we've built a simple CLI. You can use it with:
56
+ ## Install / Run
57
+ ### Option A — Prebuilt executable (recommended)
58
+ [Download from Releases](https://github.com/matbanik/Pomera-AI-Commander/releases) and run.
83
59
 
60
+ ### Option B — Python (PyPI)
84
61
  ```bash
85
- # Build the latest CLI
86
- make publisher
87
-
88
- # Use it!
89
- ./bin/mcp-publisher --help
62
+ pip install pomera-ai-commander
63
+ # then run:
64
+ pomera-ai-commander --help
90
65
  ```
91
66
 
92
- See [the publisher guide](./docs/modelcontextprotocol-io/quickstart.mdx) for more details.
67
+ ### Option C — Node.js (npm)
68
+ ```bash
69
+ npm install -g pomera-ai-commander
70
+ # then run:
71
+ pomera-mcp --help
72
+ ```
93
73
 
94
- #### Other commands
74
+ ### Create Desktop Shortcut
75
+ After installing via pip or npm, create a desktop shortcut for quick access:
95
76
 
96
77
  ```bash
97
- # Run lint, unit tests and integration tests
98
- make check
99
- ```
78
+ # For pip install:
79
+ pomera-create-shortcut
100
80
 
101
- There are also a few more helpful commands for development. Run `make help` to learn more, or look in [Makefile](./Makefile).
81
+ # For npm install (from package directory):
82
+ python create_shortcut.py
83
+ ```
102
84
 
103
- <!--
104
- For Claude and other AI tools: Always prefer make targets over custom commands where possible.
105
- -->
85
+ ---
106
86
 
107
- ## Architecture
87
+ ## MCP Server for AI Assistants
108
88
 
109
- ### Project Structure
89
+ Pomera exposes 22 text processing tools via MCP. Configure your AI assistant:
110
90
 
111
- ```
112
- ├── cmd/ # Application entry points
113
- │ └── publisher/ # Server publishing tool
114
- ├── data/ # Seed data
115
- ├── deploy/ # Deployment configuration (Pulumi)
116
- ├── docs/ # Documentation
117
- ├── internal/ # Private application code
118
- │ ├── api/ # HTTP handlers and routing
119
- │ ├── auth/ # Authentication (GitHub OAuth, JWT, namespace blocking)
120
- │ ├── config/ # Configuration management
121
- │ ├── database/ # Data persistence (PostgreSQL)
122
- │ ├── service/ # Business logic
123
- │ ├── telemetry/ # Metrics and monitoring
124
- │ └── validators/ # Input validation
125
- ├── pkg/ # Public packages
126
- │ ├── api/ # API types and structures
127
- │ │ └── v0/ # Version 0 API types
128
- │ └── model/ # Data models for server.json
129
- ├── scripts/ # Development and testing scripts
130
- ├── tests/ # Integration tests
131
- └── tools/ # CLI tools and utilities
132
- └── validate-*.sh # Schema validation tools
91
+ **Cursor** (`.cursor/mcp.json`):
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "pomera": {
96
+ "command": "pomera-ai-commander"
97
+ }
98
+ }
99
+ }
133
100
  ```
134
101
 
135
- ### Authentication
102
+ **Claude Desktop** (`claude_desktop_config.json`):
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "pomera": {
107
+ "command": "pomera-ai-commander"
108
+ }
109
+ }
110
+ }
111
+ ```
136
112
 
137
- Publishing supports multiple authentication methods:
138
- - **GitHub OAuth** - For publishing by logging into GitHub
139
- - **GitHub OIDC** - For publishing from GitHub Actions
140
- - **DNS verification** - For proving ownership of a domain and its subdomains
141
- - **HTTP verification** - For proving ownership of a domain
113
+ > **💡 Tip:** If the simple command doesn't work, use the full path. Find it with:
114
+ > ```bash
115
+ > # For npm install:
116
+ > npm root -g
117
+ > # Then use: <result>/pomera-ai-commander/pomera_mcp_server.py
118
+ >
119
+ > # For pip install:
120
+ > pip show pomera-ai-commander | grep Location
121
+ > ```
142
122
 
143
- The registry validates namespace ownership when publishing. E.g. to publish...:
144
- - `io.github.domdomegg/my-cool-mcp` you must login to GitHub as `domdomegg`, or be in a GitHub Action on domdomegg's repos
145
- - `me.adamjones/my-cool-mcp` you must prove ownership of `adamjones.me` via DNS or HTTP challenge
123
+ See the full [MCP Server Guide](docs/MCP_SERVER_GUIDE.md) for Antigravity, executable configs, and troubleshooting.
146
124
 
147
- ## Community Projects
125
+ <!-- mcp-name: io.github.matbanik/pomera -->
148
126
 
149
- Check out [community projects](docs/community-projects.md) to explore notable registry-related work created by the community.
127
+ ---
150
128
 
151
- ## More documentation
129
+ ## License
152
130
 
153
- See the [documentation](./docs) for more details if your question has not been answered here!
131
+ MIT License - see [LICENSE](LICENSE) for details.
package/mcp.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pomera-ai-commander",
3
- "version": "1.2.10",
3
+ "version": "1.3.0",
4
4
  "description": "Text processing toolkit with 22 MCP tools including case transformation, encoding, hashing, text analysis, and notes management for AI assistants.",
5
5
  "homepage": "https://github.com/matbanik/Pomera-AI-Commander",
6
6
  "repository": "https://github.com/matbanik/Pomera-AI-Commander",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pomera-ai-commander",
3
3
  "mcpName": "io.github.matbanik/pomera",
4
- "version": "1.2.10",
4
+ "version": "1.3.0",
5
5
  "description": "Text processing toolkit with 22 MCP tools for AI assistants - case transformation, encoding, hashing, text analysis, and notes management",
6
6
  "main": "pomera_mcp_server.py",
7
7
  "bin": {
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()