pressbridge 1.1.0__tar.gz
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.
- pressbridge-1.1.0/LICENSE +21 -0
- pressbridge-1.1.0/PKG-INFO +110 -0
- pressbridge-1.1.0/README.md +91 -0
- pressbridge-1.1.0/pyproject.toml +34 -0
- pressbridge-1.1.0/setup.cfg +4 -0
- pressbridge-1.1.0/src/pressbridge/__init__.py +3 -0
- pressbridge-1.1.0/src/pressbridge/__main__.py +166 -0
- pressbridge-1.1.0/src/pressbridge/audit.py +58 -0
- pressbridge-1.1.0/src/pressbridge/config.py +208 -0
- pressbridge-1.1.0/src/pressbridge/edition.py +1 -0
- pressbridge-1.1.0/src/pressbridge/files.py +313 -0
- pressbridge-1.1.0/src/pressbridge/sandbox.py +71 -0
- pressbridge-1.1.0/src/pressbridge/server.py +284 -0
- pressbridge-1.1.0/src/pressbridge/wpcli.py +214 -0
- pressbridge-1.1.0/src/pressbridge.egg-info/PKG-INFO +110 -0
- pressbridge-1.1.0/src/pressbridge.egg-info/SOURCES.txt +18 -0
- pressbridge-1.1.0/src/pressbridge.egg-info/dependency_links.txt +1 -0
- pressbridge-1.1.0/src/pressbridge.egg-info/entry_points.txt +2 -0
- pressbridge-1.1.0/src/pressbridge.egg-info/requires.txt +8 -0
- pressbridge-1.1.0/src/pressbridge.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paul Negrea
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pressbridge
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Give Claude safe, audited access to your WordPress sites over MCP
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://pressbridgesupport.github.io/pressbridge
|
|
7
|
+
Project-URL: Source, https://github.com/pressbridgesupport/pressbridge
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: mcp<3,>=2.2
|
|
12
|
+
Requires-Dist: PyYAML>=6
|
|
13
|
+
Requires-Dist: uvicorn>=0.30
|
|
14
|
+
Requires-Dist: starlette>=0.40
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
17
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# PressBridge Free
|
|
21
|
+
|
|
22
|
+
<!-- mcp-name: io.github.pressbridgesupport/pressbridge -->
|
|
23
|
+
|
|
24
|
+
**Let Claude look inside your WordPress site safely: read-only, sandboxed, secrets hidden.**
|
|
25
|
+
|
|
26
|
+
[Website](https://pressbridgesupport.github.io/pressbridge) · [Demo video](https://youtu.be/hucKMlugERw) · [Get Pro](https://pressbridgesupport.github.io/pressbridge/#pricing) · [User guide](GUIDE.md) · [Security](SECURITY.md)
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
you On mysite, where is the footer phone number set? And show me the DB password.
|
|
30
|
+
|
|
31
|
+
claude search "0744" in wp-content/themes → footer.php:41
|
|
32
|
+
read_file wp-config.php → refused: blocked by deny rule 'wp-config*'
|
|
33
|
+
|
|
34
|
+
The number is in wp-content/themes/mysite/footer.php, line 41.
|
|
35
|
+
I can't read wp-config.php: PressBridge hides it so passwords never reach me.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
PressBridge is an MCP server that gives Claude (Claude Code, Claude Desktop, any MCP
|
|
39
|
+
client) controlled access to a WordPress site. The Free edition is read-only and made
|
|
40
|
+
for one site: perfect for debugging, audits and "where is this text coming from?".
|
|
41
|
+
|
|
42
|
+
- Read, list and search theme and plugin files
|
|
43
|
+
- `wp-config.php`, `.env`, SQL dumps, keys and `.git` are invisible
|
|
44
|
+
- Nothing outside the site folder, symlink tricks included
|
|
45
|
+
- Read-only WP-CLI: versions, plugin and theme lists, checksums, cron, users
|
|
46
|
+
- `site_overview`: version and pending updates in one call
|
|
47
|
+
- Includes the **wp-care-report** skill: a client-ready health and security report
|
|
48
|
+
|
|
49
|
+
## Install (5 minutes, nothing exposed to the internet)
|
|
50
|
+
|
|
51
|
+
On the server:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
sudo apt install python3-venv php-cli php-mysql
|
|
55
|
+
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
|
|
56
|
+
sudo install wp-cli.phar /usr/local/bin/wp
|
|
57
|
+
python3 -m venv ~/pressbridge-venv
|
|
58
|
+
~/pressbridge-venv/bin/pip install ./pressbridge
|
|
59
|
+
mkdir -p ~/.pressbridge
|
|
60
|
+
cat > ~/.pressbridge/config.yaml <<EOF
|
|
61
|
+
data_dir: $HOME/.pressbridge/data
|
|
62
|
+
sites:
|
|
63
|
+
mysite:
|
|
64
|
+
root: /var/www/mysite/public_html
|
|
65
|
+
EOF
|
|
66
|
+
~/pressbridge-venv/bin/pressbridge check --config ~/.pressbridge/config.yaml
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Run it as the user that owns the site files (or one that can read them).
|
|
70
|
+
|
|
71
|
+
On your computer, Claude Code:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
claude mcp add pressbridge -- ssh you@server \
|
|
75
|
+
/home/you/pressbridge-venv/bin/pressbridge serve --config /home/you/.pressbridge/config.yaml
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Claude Desktop (`claude_desktop_config.json`, Settings → Developer → Edit Config):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"pressbridge": {
|
|
84
|
+
"command": "ssh",
|
|
85
|
+
"args": ["-o", "BatchMode=yes", "you@server",
|
|
86
|
+
"/home/you/pressbridge-venv/bin/pressbridge", "serve",
|
|
87
|
+
"--config", "/home/you/.pressbridge/config.yaml"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then ask Claude: *"Give me a health overview of mysite"* or *"Where in the theme is the
|
|
94
|
+
footer phone number set?"*
|
|
95
|
+
|
|
96
|
+
The full user guide, with every command, config option and tool, is in
|
|
97
|
+
[GUIDE.md](GUIDE.md) (features marked **Pro** aren't in this edition).
|
|
98
|
+
|
|
99
|
+
## Want Claude to fix things too?
|
|
100
|
+
|
|
101
|
+
**PressBridge Pro** adds editing with automatic backups and one-call undo, PHP syntax
|
|
102
|
+
checks before saving, plugin/theme/core updates, unlimited sites, a remote HTTPS server
|
|
103
|
+
with Docker and automatic certificates, the audit log, and skills for safe edits and
|
|
104
|
+
safe updates.
|
|
105
|
+
|
|
106
|
+
**→ https://pressbridgesupport.github.io/pressbridge**
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT. See LICENSE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# PressBridge Free
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.pressbridgesupport/pressbridge -->
|
|
4
|
+
|
|
5
|
+
**Let Claude look inside your WordPress site safely: read-only, sandboxed, secrets hidden.**
|
|
6
|
+
|
|
7
|
+
[Website](https://pressbridgesupport.github.io/pressbridge) · [Demo video](https://youtu.be/hucKMlugERw) · [Get Pro](https://pressbridgesupport.github.io/pressbridge/#pricing) · [User guide](GUIDE.md) · [Security](SECURITY.md)
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
you On mysite, where is the footer phone number set? And show me the DB password.
|
|
11
|
+
|
|
12
|
+
claude search "0744" in wp-content/themes → footer.php:41
|
|
13
|
+
read_file wp-config.php → refused: blocked by deny rule 'wp-config*'
|
|
14
|
+
|
|
15
|
+
The number is in wp-content/themes/mysite/footer.php, line 41.
|
|
16
|
+
I can't read wp-config.php: PressBridge hides it so passwords never reach me.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
PressBridge is an MCP server that gives Claude (Claude Code, Claude Desktop, any MCP
|
|
20
|
+
client) controlled access to a WordPress site. The Free edition is read-only and made
|
|
21
|
+
for one site: perfect for debugging, audits and "where is this text coming from?".
|
|
22
|
+
|
|
23
|
+
- Read, list and search theme and plugin files
|
|
24
|
+
- `wp-config.php`, `.env`, SQL dumps, keys and `.git` are invisible
|
|
25
|
+
- Nothing outside the site folder, symlink tricks included
|
|
26
|
+
- Read-only WP-CLI: versions, plugin and theme lists, checksums, cron, users
|
|
27
|
+
- `site_overview`: version and pending updates in one call
|
|
28
|
+
- Includes the **wp-care-report** skill: a client-ready health and security report
|
|
29
|
+
|
|
30
|
+
## Install (5 minutes, nothing exposed to the internet)
|
|
31
|
+
|
|
32
|
+
On the server:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sudo apt install python3-venv php-cli php-mysql
|
|
36
|
+
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
|
|
37
|
+
sudo install wp-cli.phar /usr/local/bin/wp
|
|
38
|
+
python3 -m venv ~/pressbridge-venv
|
|
39
|
+
~/pressbridge-venv/bin/pip install ./pressbridge
|
|
40
|
+
mkdir -p ~/.pressbridge
|
|
41
|
+
cat > ~/.pressbridge/config.yaml <<EOF
|
|
42
|
+
data_dir: $HOME/.pressbridge/data
|
|
43
|
+
sites:
|
|
44
|
+
mysite:
|
|
45
|
+
root: /var/www/mysite/public_html
|
|
46
|
+
EOF
|
|
47
|
+
~/pressbridge-venv/bin/pressbridge check --config ~/.pressbridge/config.yaml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run it as the user that owns the site files (or one that can read them).
|
|
51
|
+
|
|
52
|
+
On your computer, Claude Code:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
claude mcp add pressbridge -- ssh you@server \
|
|
56
|
+
/home/you/pressbridge-venv/bin/pressbridge serve --config /home/you/.pressbridge/config.yaml
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Claude Desktop (`claude_desktop_config.json`, Settings → Developer → Edit Config):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"pressbridge": {
|
|
65
|
+
"command": "ssh",
|
|
66
|
+
"args": ["-o", "BatchMode=yes", "you@server",
|
|
67
|
+
"/home/you/pressbridge-venv/bin/pressbridge", "serve",
|
|
68
|
+
"--config", "/home/you/.pressbridge/config.yaml"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then ask Claude: *"Give me a health overview of mysite"* or *"Where in the theme is the
|
|
75
|
+
footer phone number set?"*
|
|
76
|
+
|
|
77
|
+
The full user guide, with every command, config option and tool, is in
|
|
78
|
+
[GUIDE.md](GUIDE.md) (features marked **Pro** aren't in this edition).
|
|
79
|
+
|
|
80
|
+
## Want Claude to fix things too?
|
|
81
|
+
|
|
82
|
+
**PressBridge Pro** adds editing with automatic backups and one-call undo, PHP syntax
|
|
83
|
+
checks before saving, plugin/theme/core updates, unlimited sites, a remote HTTPS server
|
|
84
|
+
with Docker and automatic certificates, the audit log, and skills for safe edits and
|
|
85
|
+
safe updates.
|
|
86
|
+
|
|
87
|
+
**→ https://pressbridgesupport.github.io/pressbridge**
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT. See LICENSE.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pressbridge"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
description = "Give Claude safe, audited access to your WordPress sites over MCP"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp>=2.2,<3",
|
|
14
|
+
"PyYAML>=6",
|
|
15
|
+
"uvicorn>=0.30",
|
|
16
|
+
"starlette>=0.40",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://pressbridgesupport.github.io/pressbridge"
|
|
21
|
+
Source = "https://github.com/pressbridgesupport/pressbridge"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest>=8", "httpx>=0.27"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
pressbridge = "pressbridge.__main__:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Command line entry point.
|
|
2
|
+
|
|
3
|
+
pressbridge serve --config config.yaml # stdio (Claude Desktop / Claude Code, local)
|
|
4
|
+
pressbridge serve --config config.yaml --http # HTTPS-ready HTTP server (Pro)
|
|
5
|
+
pressbridge check --config config.yaml # validate config and WP-CLI access
|
|
6
|
+
pressbridge token # print a new random token
|
|
7
|
+
pressbridge watch run|daemon|status|report|test-alert # security watch (Agent)
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import secrets
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from .config import ConfigError, load_config
|
|
19
|
+
from .edition import EDITION
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _cmd_check(cfg) -> int:
|
|
23
|
+
from . import wpcli
|
|
24
|
+
|
|
25
|
+
ok = True
|
|
26
|
+
print(f"PressBridge {__version__} ({EDITION})")
|
|
27
|
+
print(f"Data dir: {cfg.data_dir}")
|
|
28
|
+
if not shutil.which(cfg.wp_bin):
|
|
29
|
+
print(f" ! WP-CLI '{cfg.wp_bin}' not found on PATH")
|
|
30
|
+
ok = False
|
|
31
|
+
for s in cfg.sites.values():
|
|
32
|
+
print(f"- {s.name}: root={s.root} mode={s.mode} wp={s.wp_path or 'none'} profile={s.wp_profile}")
|
|
33
|
+
if s.wp_path and shutil.which(cfg.wp_bin):
|
|
34
|
+
r = wpcli.run(cfg, s, ["core", "version"])
|
|
35
|
+
if r["exit_code"] == 0:
|
|
36
|
+
print(f" WordPress {r['stdout'].strip()}")
|
|
37
|
+
else:
|
|
38
|
+
ok = False
|
|
39
|
+
print(f" ! WP-CLI failed: {r['stderr'].strip()[:300]}")
|
|
40
|
+
print("OK" if ok else "Problems found")
|
|
41
|
+
return 0 if ok else 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _cmd_watch(cfg, a) -> int:
|
|
45
|
+
if EDITION != "agent":
|
|
46
|
+
print("PressBridge Watch is part of PressBridge Agent (https://pressbridgesupport.github.io/pressbridge).", file=sys.stderr)
|
|
47
|
+
return 2
|
|
48
|
+
from .watch import parse_settings
|
|
49
|
+
from .watch import runner
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
settings = parse_settings(cfg.watch)
|
|
53
|
+
except ConfigError as e:
|
|
54
|
+
print(f"Config error: {e}", file=sys.stderr)
|
|
55
|
+
return 2
|
|
56
|
+
|
|
57
|
+
if a.watch_cmd == "status":
|
|
58
|
+
print(runner.status(cfg, settings))
|
|
59
|
+
return 0
|
|
60
|
+
if a.watch_cmd == "report":
|
|
61
|
+
p = runner.watch_dir(cfg) / "reports" / "latest.txt"
|
|
62
|
+
print(p.read_text() if p.exists() else "No report yet. Run: pressbridge watch run")
|
|
63
|
+
return 0
|
|
64
|
+
if a.watch_cmd == "test-alert":
|
|
65
|
+
errors = runner.test_alert(settings)
|
|
66
|
+
for e in errors:
|
|
67
|
+
print(f"! {e}", file=sys.stderr)
|
|
68
|
+
if not errors:
|
|
69
|
+
print("Test alert sent.")
|
|
70
|
+
return 1 if errors else 0
|
|
71
|
+
if a.watch_cmd == "daemon":
|
|
72
|
+
try:
|
|
73
|
+
runner.daemon(cfg, settings, run_now=a.now, log=lambda m: print(m, flush=True))
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
pass
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
result = runner.run_once(cfg, settings, sites=a.site, notify=not a.no_notify,
|
|
80
|
+
use_claude=not a.no_claude, force_notify=a.notify_always,
|
|
81
|
+
save=not a.dry_run)
|
|
82
|
+
except (RuntimeError, ValueError) as e:
|
|
83
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
84
|
+
return 3
|
|
85
|
+
print(result.report_path.read_text() if result.report_path else "")
|
|
86
|
+
if result.notified:
|
|
87
|
+
print("Alert sent.")
|
|
88
|
+
for e in result.delivery_errors:
|
|
89
|
+
print(f"! Alert delivery failed: {e}", file=sys.stderr)
|
|
90
|
+
return result.exit_code
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main(argv: list[str] | None = None) -> int:
|
|
94
|
+
p = argparse.ArgumentParser(prog="pressbridge", description="Safe WordPress access for Claude")
|
|
95
|
+
p.add_argument("--version", action="version", version=f"pressbridge {__version__} ({EDITION})")
|
|
96
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
97
|
+
|
|
98
|
+
serve = sub.add_parser("serve", help="Run the MCP server")
|
|
99
|
+
serve.add_argument("--config", default=os.environ.get("PRESSBRIDGE_CONFIG", "/config/config.yaml"))
|
|
100
|
+
serve.add_argument("--http", action="store_true", help="Serve over HTTP instead of stdio (Pro)")
|
|
101
|
+
serve.add_argument("--port", type=int, default=int(os.environ.get("PRESSBRIDGE_PORT", "8765")))
|
|
102
|
+
serve.add_argument("--host", default=os.environ.get("PRESSBRIDGE_HOST", "0.0.0.0"),
|
|
103
|
+
help="Interface to bind in HTTP mode (use 127.0.0.1 behind a proxy on the host network)")
|
|
104
|
+
|
|
105
|
+
check = sub.add_parser("check", help="Validate the config and WP-CLI access")
|
|
106
|
+
check.add_argument("--config", default=os.environ.get("PRESSBRIDGE_CONFIG", "/config/config.yaml"))
|
|
107
|
+
|
|
108
|
+
sub.add_parser("token", help="Generate a random bearer token")
|
|
109
|
+
|
|
110
|
+
watch = sub.add_parser("watch", help="Security watch: nightly checks and alerts (Agent edition)")
|
|
111
|
+
watch.add_argument("--config", default=os.environ.get("PRESSBRIDGE_CONFIG", "/config/config.yaml"))
|
|
112
|
+
wsub = watch.add_subparsers(dest="watch_cmd", required=True)
|
|
113
|
+
w_run = wsub.add_parser("run", help="Check all sites once now")
|
|
114
|
+
w_run.add_argument("--site", action="append", help="Only this site (repeatable)")
|
|
115
|
+
w_run.add_argument("--no-notify", action="store_true", help="Don't send alerts")
|
|
116
|
+
w_run.add_argument("--no-claude", action="store_true", help="Skip Claude's review")
|
|
117
|
+
w_run.add_argument("--notify-always", action="store_true", help="Send the report even if all is clear")
|
|
118
|
+
w_run.add_argument("--dry-run", action="store_true", help="Don't save the new baseline (compare only)")
|
|
119
|
+
w_daemon = wsub.add_parser("daemon", help="Run on the schedule, forever (for Docker or systemd)")
|
|
120
|
+
w_daemon.add_argument("--now", action="store_true", help="Also run once at start")
|
|
121
|
+
wsub.add_parser("status", help="Show the last run, the next run and each site's baseline")
|
|
122
|
+
wsub.add_parser("test-alert", help="Send a test alert to every configured channel")
|
|
123
|
+
wsub.add_parser("report", help="Print the latest report")
|
|
124
|
+
|
|
125
|
+
a = p.parse_args(argv)
|
|
126
|
+
if a.cmd == "token":
|
|
127
|
+
print(secrets.token_hex(32))
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
cfg = load_config(a.config)
|
|
132
|
+
except ConfigError as e:
|
|
133
|
+
print(f"Config error: {e}", file=sys.stderr)
|
|
134
|
+
return 2
|
|
135
|
+
|
|
136
|
+
if a.cmd == "check":
|
|
137
|
+
return _cmd_check(cfg)
|
|
138
|
+
|
|
139
|
+
if a.cmd == "watch":
|
|
140
|
+
return _cmd_watch(cfg, a)
|
|
141
|
+
|
|
142
|
+
if a.http:
|
|
143
|
+
if EDITION == "free":
|
|
144
|
+
print("HTTP mode is a PressBridge Pro feature. Free runs over stdio.", file=sys.stderr)
|
|
145
|
+
return 2
|
|
146
|
+
import uvicorn
|
|
147
|
+
|
|
148
|
+
from .server import build_http_app
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
app = build_http_app(cfg)
|
|
152
|
+
except ConfigError as e:
|
|
153
|
+
print(f"Config error: {e}", file=sys.stderr)
|
|
154
|
+
return 2
|
|
155
|
+
uvicorn.run(app, host=a.host, port=a.port, log_level="info", proxy_headers=True,
|
|
156
|
+
forwarded_allow_ips="*")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
from .server import build_server
|
|
160
|
+
|
|
161
|
+
build_server(cfg).run("stdio")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
sys.exit(main())
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Append-only JSON Lines audit log of every tool call."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_lock = threading.Lock()
|
|
10
|
+
_MAX_FIELD = 500
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _shorten(value):
|
|
14
|
+
if isinstance(value, str) and len(value) > _MAX_FIELD:
|
|
15
|
+
return value[:_MAX_FIELD] + f"... [{len(value)} chars]"
|
|
16
|
+
if isinstance(value, list):
|
|
17
|
+
return [_shorten(v) for v in value]
|
|
18
|
+
if isinstance(value, dict):
|
|
19
|
+
return {k: _shorten(v) for k, v in value.items()}
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def record(log_path: Path, *, tool: str, site: str | None, args: dict, ok: bool, error: str | None = None,
|
|
24
|
+
result_summary: dict | None = None) -> None:
|
|
25
|
+
entry = {
|
|
26
|
+
"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
27
|
+
"tool": tool,
|
|
28
|
+
"site": site,
|
|
29
|
+
"args": _shorten(args),
|
|
30
|
+
"ok": ok,
|
|
31
|
+
}
|
|
32
|
+
if error:
|
|
33
|
+
entry["error"] = error[:_MAX_FIELD]
|
|
34
|
+
if result_summary:
|
|
35
|
+
entry["result"] = result_summary
|
|
36
|
+
line = json.dumps(entry, ensure_ascii=False)
|
|
37
|
+
with _lock:
|
|
38
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
with log_path.open("a", encoding="utf-8") as f:
|
|
40
|
+
f.write(line + "\n")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tail(log_path: Path, limit: int = 50, site: str | None = None) -> list[dict]:
|
|
44
|
+
if not log_path.exists():
|
|
45
|
+
return []
|
|
46
|
+
lines = log_path.read_text(encoding="utf-8").splitlines()
|
|
47
|
+
out: list[dict] = []
|
|
48
|
+
for line in reversed(lines):
|
|
49
|
+
try:
|
|
50
|
+
item = json.loads(line)
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
continue
|
|
53
|
+
if site and item.get("site") != site:
|
|
54
|
+
continue
|
|
55
|
+
out.append(item)
|
|
56
|
+
if len(out) >= limit:
|
|
57
|
+
break
|
|
58
|
+
return out
|