ultimate-pi 0.4.1 → 0.6.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.
@@ -6,6 +6,8 @@ import os
6
6
  from dataclasses import dataclass
7
7
  from urllib.parse import urlparse
8
8
 
9
+ SUPPORTED_SEARCH_ENGINES = frozenset({"ddg_html", "searxng"})
10
+
9
11
 
10
12
  def _int_env(name: str, default: int) -> int:
11
13
  raw = os.environ.get(name, "").strip()
@@ -24,6 +26,18 @@ def _fetch_mode() -> str:
24
26
  return "stealth"
25
27
 
26
28
 
29
+ def _normalize_searxng_url(raw: str) -> str:
30
+ url = raw.strip().rstrip("/")
31
+ if not url:
32
+ return ""
33
+ parsed = urlparse(url)
34
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
35
+ raise SystemExit(
36
+ f"Invalid HARNESS_WEB_SEARXNG_URL={raw!r} — expected http(s)://host[:port]"
37
+ )
38
+ return url
39
+
40
+
27
41
  _STATIC_HOSTS = frozenset(
28
42
  {
29
43
  "example.com",
@@ -50,6 +64,7 @@ def host_is_static(url: str) -> bool:
50
64
  class HarnessWebConfig:
51
65
  fetch_mode: str
52
66
  search_engine: str
67
+ searxng_url: str | None
53
68
  proxy: str | None
54
69
  rate_limit_ms: int
55
70
  timeout_ms: int
@@ -68,13 +83,32 @@ class HarnessWebConfig:
68
83
  return False
69
84
 
70
85
 
86
+ def validate_search_config(config: HarnessWebConfig) -> None:
87
+ engine = config.search_engine
88
+ if engine not in SUPPORTED_SEARCH_ENGINES:
89
+ supported = ", ".join(sorted(SUPPORTED_SEARCH_ENGINES))
90
+ raise SystemExit(
91
+ f"Unsupported HARNESS_WEB_SEARCH_ENGINE={engine!r} (supported: {supported})"
92
+ )
93
+ if engine == "searxng" and not config.searxng_url:
94
+ raise SystemExit(
95
+ "HARNESS_WEB_SEARCH_ENGINE=searxng requires HARNESS_WEB_SEARXNG_URL "
96
+ "(e.g. http://127.0.0.1:8080). Run /harness-setup and choose SearXNG, or set both in .env."
97
+ )
98
+
99
+
71
100
  def load_config() -> HarnessWebConfig:
72
101
  proxy = os.environ.get("HARNESS_WEB_PROXY", "").strip() or None
73
- return HarnessWebConfig(
102
+ engine = os.environ.get("HARNESS_WEB_SEARCH_ENGINE", "ddg_html").strip() or "ddg_html"
103
+ searx_raw = os.environ.get("HARNESS_WEB_SEARXNG_URL", "").strip()
104
+ searxng_url = _normalize_searxng_url(searx_raw) if searx_raw else None
105
+ config = HarnessWebConfig(
74
106
  fetch_mode=_fetch_mode(),
75
- search_engine=os.environ.get("HARNESS_WEB_SEARCH_ENGINE", "ddg_html").strip()
76
- or "ddg_html",
107
+ search_engine=engine,
108
+ searxng_url=searxng_url,
77
109
  proxy=proxy,
78
110
  rate_limit_ms=_int_env("HARNESS_WEB_RATE_LIMIT_MS", 2000),
79
111
  timeout_ms=_int_env("HARNESS_WEB_TIMEOUT_MS", 30000),
80
112
  )
113
+ validate_search_config(config)
114
+ return config
@@ -18,13 +18,19 @@ def write_json(path: Path, payload: Any) -> None:
18
18
  path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
19
19
 
20
20
 
21
- def write_search_results(path: Path, results: list[dict[str, str]], query: str) -> None:
21
+ def write_search_results(
22
+ path: Path,
23
+ results: list[dict[str, str]],
24
+ query: str,
25
+ *,
26
+ engine: str,
27
+ ) -> None:
22
28
  """Firecrawl-compatible envelope: data.web[].url|title|description."""
23
29
  write_json(
24
30
  path,
25
31
  {
26
32
  "query": query,
27
- "engine": "ddg_html",
33
+ "engine": engine,
28
34
  "data": {
29
35
  "web": [
30
36
  {
@@ -0,0 +1,22 @@
1
+ """Route harness-web search to the configured SERP backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .config import HarnessWebConfig, validate_search_config
6
+ from .search_ddg import search_ddg
7
+ from .search_searxng import search_searxng
8
+
9
+
10
+ def search(
11
+ query: str,
12
+ *,
13
+ limit: int,
14
+ config: HarnessWebConfig,
15
+ ) -> list[dict[str, str]]:
16
+ validate_search_config(config)
17
+ engine = config.search_engine
18
+ if engine == "searxng":
19
+ return search_searxng(query, limit=limit, config=config)
20
+ if engine == "ddg_html":
21
+ return search_ddg(query, limit=limit, config=config)
22
+ raise SystemExit(f"Unsupported HARNESS_WEB_SEARCH_ENGINE={engine!r}")
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Any
5
6
  from urllib.parse import parse_qs, unquote, urlparse
6
7
 
7
8
  from scrapling.fetchers import Fetcher, StealthyFetcher
@@ -63,11 +64,6 @@ def search_ddg(
63
64
  config: HarnessWebConfig,
64
65
  impersonate: bool = True,
65
66
  ) -> list[dict[str, str]]:
66
- if config.search_engine != "ddg_html":
67
- raise SystemExit(
68
- f"Unsupported HARNESS_WEB_SEARCH_ENGINE={config.search_engine!r} (only ddg_html)"
69
- )
70
-
71
67
  kwargs: dict = {
72
68
  "params": {"q": query},
73
69
  "timeout": config.timeout_sec,
@@ -0,0 +1,100 @@
1
+ """SearXNG JSON search API (self-hosted instances)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import ssl
7
+ from typing import Any
8
+ from urllib.error import HTTPError, URLError
9
+ from urllib.parse import urlencode
10
+ from urllib.request import ProxyHandler, Request, build_opener, urlopen
11
+
12
+ from .config import HarnessWebConfig
13
+
14
+
15
+ def _open_url(url: str, *, config: HarnessWebConfig) -> tuple[int, str]:
16
+ req = Request(
17
+ url,
18
+ headers={"Accept": "application/json", "User-Agent": "ultimate-pi-harness-web/1.0"},
19
+ method="GET",
20
+ )
21
+ if config.proxy:
22
+ opener = build_opener(ProxyHandler({"http": config.proxy, "https": config.proxy}))
23
+ resp = opener.open(req, timeout=config.timeout_sec)
24
+ else:
25
+ ctx = ssl.create_default_context()
26
+ resp = urlopen(req, timeout=config.timeout_sec, context=ctx)
27
+ try:
28
+ status = getattr(resp, "status", 200) or 200
29
+ body = resp.read().decode("utf-8", errors="replace")
30
+ return status, body
31
+ finally:
32
+ resp.close()
33
+
34
+
35
+ def _parse_results(payload: Any, limit: int) -> list[dict[str, str]]:
36
+ raw = payload.get("results") if isinstance(payload, dict) else None
37
+ if not isinstance(raw, list):
38
+ return []
39
+ out: list[dict[str, str]] = []
40
+ for item in raw:
41
+ if len(out) >= limit:
42
+ break
43
+ if not isinstance(item, dict):
44
+ continue
45
+ url = (item.get("url") or "").strip()
46
+ if not url.startswith("http"):
47
+ continue
48
+ title = (item.get("title") or "").strip()
49
+ description = (item.get("content") or item.get("snippet") or "").strip()
50
+ out.append({"url": url, "title": title, "description": description})
51
+ return out
52
+
53
+
54
+ def search_searxng(
55
+ query: str,
56
+ *,
57
+ limit: int,
58
+ config: HarnessWebConfig,
59
+ ) -> list[dict[str, str]]:
60
+ base = config.searxng_url
61
+ if not base:
62
+ raise SystemExit("HARNESS_WEB_SEARXNG_URL is not set")
63
+
64
+ qs = urlencode({"q": query, "format": "json", "pageno": "1"})
65
+ url = f"{base}/search?{qs}"
66
+
67
+ try:
68
+ status, body = _open_url(url, config=config)
69
+ except HTTPError as err:
70
+ status = err.code
71
+ body = err.read().decode("utf-8", errors="replace") if err.fp else ""
72
+ except URLError as err:
73
+ raise SystemExit(
74
+ f"SearXNG request failed ({err.reason}). "
75
+ f"Is the instance running at {base}? "
76
+ "Run: node \"$UP_PKG/.pi/scripts/harness-searxng-bootstrap.mjs\""
77
+ ) from err
78
+
79
+ if status == 403:
80
+ raise SystemExit(
81
+ "SearXNG returned 403 for format=json. Enable json under search.formats "
82
+ "in settings.yml (see .searxng/core-config/settings.yml or SearXNG docs)."
83
+ )
84
+ if status != 200:
85
+ snippet = body[:200].replace("\n", " ")
86
+ raise SystemExit(f"SearXNG search failed (HTTP {status}): {snippet}")
87
+
88
+ try:
89
+ payload = json.loads(body)
90
+ except json.JSONDecodeError as err:
91
+ raise SystemExit(f"SearXNG returned non-JSON response from {url}") from err
92
+
93
+ results = _parse_results(payload, limit)
94
+ if not results and isinstance(payload, dict):
95
+ unresponsive = payload.get("unresponsive_engines")
96
+ if unresponsive:
97
+ raise SystemExit(
98
+ f"SearXNG returned no results; upstream engines unresponsive: {unresponsive}"
99
+ )
100
+ return results
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.6.0] — 2026-05-17
8
+
9
+ ### ✨ Features
10
+
11
+ - **sentrux Pi skill:** CLI-first architectural quality workflows (`check`, `gate`, GUI) via `/skill:sentrux`; symlinked in `.pi/skills`. Pi does not load `.pi/mcp.json`.
12
+
13
+ ### 📖 Documentation
14
+
15
+ - **harness-setup / CONTRIBUTING:** document Sentrux skill instead of MCP config; update `harness-sentrux-setup` workflow.
16
+
17
+ ### 🔧 Chores
18
+
19
+ - Remove shipped `.pi/mcp.json` from package `files` list; refresh `graphify-out`.
20
+
21
+ ## [v0.5.0] — 2026-05-17
22
+
23
+ ### ✨ Features
24
+
25
+ - **web_search / web_fetch pi tools:** wrap `harness-web.py` with session injection and a bash guard so agents skip `UP_PKG` and scrapling import preflights.
26
+ - **SearXNG search backend:** pluggable `HARNESS_WEB_SEARCH_ENGINE` (`ddg_html` | `searxng`) with Docker bootstrap via `harness-searxng-bootstrap.mjs`.
27
+ - **harness-web status:** JSON config subcommand for setup and diagnostics.
28
+
29
+ ### 🔧 Chores
30
+
31
+ - Apply pre-commit format and refresh `graphify-out` after harness-web tools merge.
32
+
7
33
  ## [v0.4.1] — 2026-05-17
8
34
 
9
35
  ### ✨ Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-pi",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Ultimate AI coding harness for pi.dev — extensible skills, Obsidian wiki knowledge layer, compressed context, deterministic output",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -59,7 +59,6 @@
59
59
  ".pi/model-router.example.json",
60
60
  ".pi/settings.example.json",
61
61
  ".pi/auto-commit.json",
62
- ".pi/mcp.json",
63
62
  ".pi/SYSTEM.md",
64
63
  ".pi/PACKAGING.md",
65
64
  "AGENTS.md",
@@ -74,7 +73,7 @@
74
73
  "@mariozechner/pi-coding-agent": "*"
75
74
  },
76
75
  "scripts": {
77
- "check:ts": "tsc --noEmit --target ES2023 --lib ES2023 --moduleResolution nodenext --module nodenext --skipLibCheck .pi/extensions/lib/harness-vcc-settings.ts .pi/extensions/dotenv-loader.ts .pi/extensions/lib/posthog-node.d.ts .pi/extensions/lib/harness-posthog.ts .pi/extensions/lib/harness-paths.ts .pi/extensions/pi-model-router-harness.ts .pi/extensions/provider-payload-sanitize.ts .pi/extensions/harness-telemetry.ts .pi/extensions/harness-ask-user.ts .pi/extensions/lib/ask-user/schema.ts .pi/extensions/lib/ask-user/types.ts .pi/extensions/lib/ask-user/validate.ts .pi/extensions/lib/ask-user/dialog.ts .pi/extensions/lib/ask-user/fallback.ts .pi/extensions/lib/ask-user/render.ts .pi/extensions/trace-recorder.ts .pi/extensions/observation-bus.ts .pi/extensions/drift-monitor.ts .pi/extensions/sentrux-rules-sync.ts .pi/extensions/custom-header.ts .pi/extensions/lib/harness-subagents/agent-loader.ts .pi/extensions/lib/harness-subagents/agent-parser.ts .pi/extensions/lib/harness-subagents/agent-manifest.ts .pi/extensions/lib/harness-subagents/blackboard.ts .pi/extensions/lib/harness-subagents/blackboard-tool.ts .pi/extensions/lib/harness-subagents/spawn-policy.ts .pi/extensions/lib/harness-subagents/types-blackboard.ts",
76
+ "check:ts": "tsc --noEmit --target ES2023 --lib ES2023 --moduleResolution nodenext --module nodenext --skipLibCheck .pi/extensions/lib/harness-vcc-settings.ts .pi/extensions/dotenv-loader.ts .pi/extensions/lib/posthog-node.d.ts .pi/extensions/lib/harness-posthog.ts .pi/extensions/lib/harness-paths.ts .pi/extensions/pi-model-router-harness.ts .pi/extensions/provider-payload-sanitize.ts .pi/extensions/harness-telemetry.ts .pi/extensions/harness-ask-user.ts .pi/extensions/lib/ask-user/schema.ts .pi/extensions/lib/ask-user/types.ts .pi/extensions/lib/ask-user/validate.ts .pi/extensions/lib/ask-user/dialog.ts .pi/extensions/lib/ask-user/fallback.ts .pi/extensions/lib/ask-user/render.ts .pi/extensions/trace-recorder.ts .pi/extensions/observation-bus.ts .pi/extensions/drift-monitor.ts .pi/extensions/sentrux-rules-sync.ts .pi/extensions/custom-header.ts .pi/extensions/lib/harness-subagents/agent-loader.ts .pi/extensions/lib/harness-subagents/agent-parser.ts .pi/extensions/lib/harness-subagents/agent-manifest.ts .pi/extensions/lib/harness-subagents/blackboard.ts .pi/extensions/lib/harness-subagents/blackboard-tool.ts .pi/extensions/lib/harness-subagents/spawn-policy.ts .pi/extensions/lib/harness-subagents/types-blackboard.ts .pi/extensions/harness-web-tools.ts .pi/extensions/harness-web-guard.ts .pi/extensions/lib/harness-web/run-cli.ts",
78
77
  "vendor:sync-router": "bash .pi/scripts/vendor-sync-pi-model-router.sh",
79
78
  "vendor:sync-vcc": "bash .pi/scripts/vendor-sync-pi-vcc.sh",
80
79
  "release": "bash .pi/scripts/release.sh",
package/.pi/mcp.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "context-mode": {
4
- "command": "context-mode"
5
- },
6
- "sentrux": {
7
- "command": "sentrux",
8
- "args": ["--mcp"]
9
- }
10
- }
11
- }