pomera-ai-commander 1.2.11 → 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 +1 -1
- 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
|
@@ -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
|
-
>
|
|
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,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()
|