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 +90 -112
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/pomera.py +167 -7
- package/tools/validate_version.py +196 -0
package/README.md
CHANGED
|
@@ -1,153 +1,131 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Pomera AI Commander (PAC)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
[
|
|
7
|
+
[](https://github.com/matbanik/Pomera-AI-Commander/releases)
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
17
|
+
## 60-second demo (what to expect)
|
|
18
|
+

|
|
20
19
|
|
|
21
|
-
|
|
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
|
-
|
|
27
|
+
---
|
|
24
28
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
52
|
+
> **Note:** For PEP 668 protected environments, use `pip3 install --user` or a virtual environment.
|
|
79
53
|
|
|
80
|
-
|
|
54
|
+
---
|
|
81
55
|
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
### Create Desktop Shortcut
|
|
75
|
+
After installing via pip or npm, create a desktop shortcut for quick access:
|
|
95
76
|
|
|
96
77
|
```bash
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
```
|
|
78
|
+
# For pip install:
|
|
79
|
+
pomera-create-shortcut
|
|
100
80
|
|
|
101
|
-
|
|
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
|
-
##
|
|
87
|
+
## MCP Server for AI Assistants
|
|
108
88
|
|
|
109
|
-
|
|
89
|
+
Pomera exposes 22 text processing tools via MCP. Configure your AI assistant:
|
|
110
90
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
<!-- mcp-name: io.github.matbanik/pomera -->
|
|
148
126
|
|
|
149
|
-
|
|
127
|
+
---
|
|
150
128
|
|
|
151
|
-
##
|
|
129
|
+
## License
|
|
152
130
|
|
|
153
|
-
|
|
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.
|
|
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.
|
|
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"), ("
|
|
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.
|
|
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 == "
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
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()
|