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.
- package/.agents/skills/harness-decisions/SKILL.md +15 -0
- package/.agents/skills/harness-sentrux-setup/SKILL.md +1 -1
- package/.agents/skills/scrapling-web/SKILL.md +45 -40
- package/.agents/skills/sentrux/SKILL.md +99 -0
- package/.agents/skills/wiki-autoresearch/SKILL.md +3 -3
- package/.pi/SYSTEM.md +12 -13
- package/.pi/agents/pi-pi/agent-expert.md +3 -3
- package/.pi/extensions/harness-web-guard.ts +95 -0
- package/.pi/extensions/harness-web-tools.ts +209 -0
- package/.pi/extensions/lib/harness-web/run-cli.ts +92 -0
- package/.pi/harness/env.harness.template +3 -1
- package/.pi/prompts/harness-setup.md +66 -21
- package/.pi/scripts/harness-cli-verify.sh +12 -3
- package/.pi/scripts/harness-searxng-bootstrap.mjs +270 -0
- package/.pi/scripts/harness-web-search.md +24 -5
- package/.pi/scripts/harness-web.py +24 -7
- package/.pi/scripts/harness_web/config.py +37 -3
- package/.pi/scripts/harness_web/output.py +8 -2
- package/.pi/scripts/harness_web/search.py +22 -0
- package/.pi/scripts/harness_web/search_ddg.py +1 -5
- package/.pi/scripts/harness_web/search_searxng.py +100 -0
- package/CHANGELOG.md +26 -0
- package/package.json +2 -3
- package/.pi/mcp.json +0 -11
|
@@ -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
|
-
|
|
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=
|
|
76
|
-
|
|
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(
|
|
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":
|
|
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.
|
|
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",
|