pi-lean-search 0.1.0 → 0.2.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 +6 -2
- package/index.ts +4 -21
- package/package.json +2 -2
- package/verify-ship-manifest.ts +6 -92
- package/web-search-tool.ts +19 -13
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# pi-lean-search
|
|
2
2
|
|
|
3
|
-
> SearXNG search tool for Pi.
|
|
3
|
+
> SearXNG search tool for Pi. Pairs with [pi-lean-portal](https://www.npmjs.com/package/pi-lean-portal)'s
|
|
4
|
+
> `/web` toggle — search-only installs are valid, or add it to a portal install
|
|
5
|
+
> for the full web-tools suite. Part of the
|
|
6
|
+
> [pi-lean-dimension](https://github.com/coreyryanhanson/pi-lean-dimension)
|
|
7
|
+
> web-tools suite.
|
|
4
8
|
|
|
5
9
|
## Quick start
|
|
6
10
|
|
|
@@ -19,7 +23,7 @@ pi install npm:pi-lean-dimension # portal + search, requires SearXNG server
|
|
|
19
23
|
|
|
20
24
|
| Command / Tool | Description |
|
|
21
25
|
|---|---|
|
|
22
|
-
| `web-search` tool | Search the web via your SearXNG instance. Agents use this automatically. |
|
|
26
|
+
| `web-search` tool | Search the web via your SearXNG instance. Supports `count` (1–100) and `pageno` (1-indexed) for deeper result pagination. Agents use this automatically. |
|
|
23
27
|
| `/searxng-status` | Test and diagnose the SearXNG connection. |
|
|
24
28
|
|
|
25
29
|
The `web-search` tool is automatically included in `/web on` / `/web off` toggling
|
package/index.ts
CHANGED
|
@@ -44,8 +44,8 @@ async function checkServerReachable(
|
|
|
44
44
|
let res: Response;
|
|
45
45
|
try {
|
|
46
46
|
const mergedSignal = signal
|
|
47
|
-
?
|
|
48
|
-
:
|
|
47
|
+
? AbortSignal.any([signal, controller.signal])
|
|
48
|
+
: controller.signal;
|
|
49
49
|
res = await fetch(url, { signal: mergedSignal });
|
|
50
50
|
} finally {
|
|
51
51
|
clearTimeout(timeoutId);
|
|
@@ -75,8 +75,8 @@ async function checkSearchReachable(
|
|
|
75
75
|
let res: Response;
|
|
76
76
|
try {
|
|
77
77
|
const mergedSignal = signal
|
|
78
|
-
?
|
|
79
|
-
:
|
|
78
|
+
? AbortSignal.any([signal, controller.signal])
|
|
79
|
+
: controller.signal;
|
|
80
80
|
res = await fetch(searchUrl, {
|
|
81
81
|
signal: mergedSignal,
|
|
82
82
|
headers: { Accept: "application/json" },
|
|
@@ -95,23 +95,6 @@ async function checkSearchReachable(
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/**
|
|
99
|
-
* Combine two AbortSignals into one that aborts when either aborts.
|
|
100
|
-
*/
|
|
101
|
-
function combineAbortSignals(...signals: AbortSignal[]): AbortSignal {
|
|
102
|
-
const controller = new AbortController();
|
|
103
|
-
for (const sig of signals) {
|
|
104
|
-
if (sig.aborted) {
|
|
105
|
-
controller.abort(sig.reason);
|
|
106
|
-
return controller.signal;
|
|
107
|
-
}
|
|
108
|
-
sig.addEventListener("abort", () => controller.abort(sig.reason), {
|
|
109
|
-
once: true,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
return controller.signal;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
98
|
// ─── Status slot helpers ──────────────────────────────────────────
|
|
116
99
|
|
|
117
100
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-search",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension. SearXNG search tool for Pi
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pi extension. SearXNG search tool for Pi — pairs with pi-lean-portal's /web toggle; search-only installs are valid, or add it to a portal install for the full web-tools suite.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi-extension",
|
package/verify-ship-manifest.ts
CHANGED
|
@@ -1,96 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ship-manifest verification helper.
|
|
2
|
+
* Ship-manifest verification helper (thin re-export).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* actually contains the modules it imports at runtime.
|
|
8
|
-
*
|
|
9
|
-
* Ported from pi-lean-portal's verify-ship-manifest.ts (rpiv-mono pattern).
|
|
4
|
+
* Re-exports the shared {@link verifyShipManifest} from pi-lean-portal's
|
|
5
|
+
* core/shared/ship-manifest.ts. Kept so `ship-manifest.test.ts` can import
|
|
6
|
+
* from a local path without depending on a specific cross-package layout.
|
|
10
7
|
*/
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
|
|
16
|
-
const SKIP_DIRS = new Set(["node_modules", "docs", "__tests__"]);
|
|
17
|
-
const SKIP_FILES = new Set(["test-fixtures.ts"]);
|
|
18
|
-
/** Entries that won't exist on disk at rest but are valid (generated at pack time, e.g. by prepack). */
|
|
19
|
-
const SKIP_STALE = new Set(["LICENSE"]);
|
|
20
|
-
|
|
21
|
-
export interface ShipManifestResult {
|
|
22
|
-
declared: readonly string[];
|
|
23
|
-
onDisk: readonly string[];
|
|
24
|
-
missing: readonly string[];
|
|
25
|
-
stale: readonly string[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function verifyShipManifest(
|
|
29
|
-
packageDirOrUrl: string,
|
|
30
|
-
): ShipManifestResult {
|
|
31
|
-
const packageDir = packageDirOrUrl.startsWith("file:")
|
|
32
|
-
? dirname(fileURLToPath(packageDirOrUrl))
|
|
33
|
-
: packageDirOrUrl;
|
|
34
|
-
const pkgRaw = readFileSync(resolve(packageDir, "package.json"), "utf8");
|
|
35
|
-
const pkg = JSON.parse(pkgRaw) as { files?: string[] };
|
|
36
|
-
const declared = pkg.files ?? [];
|
|
37
|
-
const exactFiles = new Set<string>();
|
|
38
|
-
const dirPrefixes: string[] = [];
|
|
39
|
-
for (const entry of declared) {
|
|
40
|
-
// Negation patterns (`!foo/`) are npm `files`-array syntax for excluding
|
|
41
|
-
// sub-paths within an included directory. They have no on-disk counterpart;
|
|
42
|
-
// skip them for existence checks and staleness detection.
|
|
43
|
-
if (entry.startsWith("!")) continue;
|
|
44
|
-
if (entry.endsWith("/")) dirPrefixes.push(entry);
|
|
45
|
-
else if (isDirOnDisk(packageDir, entry)) dirPrefixes.push(`${entry}/`);
|
|
46
|
-
else exactFiles.add(entry);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const onDisk = walkProductionTs(packageDir, packageDir);
|
|
50
|
-
const missing = onDisk.filter((f) => !isCovered(f, exactFiles, dirPrefixes));
|
|
51
|
-
const stale = declared.filter(
|
|
52
|
-
(entry) =>
|
|
53
|
-
!entry.startsWith("!") &&
|
|
54
|
-
!SKIP_STALE.has(entry) &&
|
|
55
|
-
!existsSync(resolve(packageDir, entry)),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
return { declared, onDisk, missing, stale };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isDirOnDisk(packageDir: string, entry: string): boolean {
|
|
62
|
-
try {
|
|
63
|
-
return statSync(resolve(packageDir, entry)).isDirectory();
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isCovered(
|
|
70
|
-
file: string,
|
|
71
|
-
exactFiles: Set<string>,
|
|
72
|
-
dirPrefixes: readonly string[],
|
|
73
|
-
): boolean {
|
|
74
|
-
if (exactFiles.has(file)) return true;
|
|
75
|
-
for (const prefix of dirPrefixes) {
|
|
76
|
-
if (file.startsWith(prefix)) return true;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function walkProductionTs(root: string, dir: string): string[] {
|
|
82
|
-
const out: string[] = [];
|
|
83
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
84
|
-
if (entry.name.startsWith(".")) continue;
|
|
85
|
-
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
86
|
-
const abs = resolve(dir, entry.name);
|
|
87
|
-
if (entry.isDirectory()) {
|
|
88
|
-
out.push(...walkProductionTs(root, abs));
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (!entry.isFile() || !entry.name.endsWith(".ts")) continue;
|
|
92
|
-
if (entry.name.endsWith(".test.ts") || SKIP_FILES.has(entry.name)) continue;
|
|
93
|
-
out.push(relative(root, abs));
|
|
94
|
-
}
|
|
95
|
-
return out;
|
|
96
|
-
}
|
|
9
|
+
export type { ShipManifestResult } from "../pi-lean-portal/core/shared/ship-manifest.js";
|
|
10
|
+
export { verifyShipManifest } from "../pi-lean-portal/core/shared/ship-manifest.js";
|
package/web-search-tool.ts
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* web-search tool
|
|
3
|
-
*
|
|
4
|
-
* Searches the web via a SearXNG instance. Degrades gracefully when
|
|
5
|
-
* no SearXNG URL is configured — returns a setup message on first call.
|
|
6
|
-
*
|
|
7
|
-
* Adapted from the prototype at:
|
|
8
|
-
* /root/lab/startup_scripts/firecracker/config/pi/extensions/searxng-search/index.ts
|
|
9
|
-
*
|
|
10
|
-
* Changes from prototype:
|
|
11
|
-
* - Config read from Pi settings.json (searxng.url) instead of env vars
|
|
12
|
-
* - No injectUnavailabilityNotice (graceful degradation via tool output only)
|
|
13
|
-
* - Health state management lives in index.ts, not in the tool
|
|
2
|
+
* web-search tool for pi-lean-search — SearXNG web search with graceful degradation.
|
|
3
|
+
* Config read from Pi settings.json; health state managed in index.ts.
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
6
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
@@ -41,6 +31,7 @@ function buildSearchUrl(
|
|
|
41
31
|
query: string,
|
|
42
32
|
options: {
|
|
43
33
|
count: number;
|
|
34
|
+
pageno: number;
|
|
44
35
|
language: string;
|
|
45
36
|
safesearch: string;
|
|
46
37
|
time_range: string;
|
|
@@ -60,6 +51,7 @@ function buildSearchUrl(
|
|
|
60
51
|
if (options.safesearch) params.set("safesearch", options.safesearch);
|
|
61
52
|
if (options.time_range) params.set("time_range", options.time_range);
|
|
62
53
|
if (options.category) params.set("categories", options.category);
|
|
54
|
+
if (options.pageno > 1) params.set("pageno", String(options.pageno));
|
|
63
55
|
if (options.engines) params.set("engines", options.engines);
|
|
64
56
|
|
|
65
57
|
return `${normalized}/search?${params.toString()}`;
|
|
@@ -75,8 +67,10 @@ export const webSearchTool = defineTool({
|
|
|
75
67
|
"Use for finding current information, research, news, and fact-checking.",
|
|
76
68
|
promptSnippet:
|
|
77
69
|
"Search the web via a local SearXNG instance — use for up-to-date facts, verification, or research.",
|
|
78
|
-
promptGuidelines:
|
|
70
|
+
promptGuidelines: [
|
|
79
71
|
'Use when you need recent/current information not already known. Increase `count` for broad research; keep it small for quick lookups. Filter by time_range="day" for breaking news, category="news" for journalism. Set language to match the query (e.g. "de" for German, "es" for Spanish).',
|
|
72
|
+
"Use `pageno` (1-indexed) to fetch deeper pages when `count` results are not enough.",
|
|
73
|
+
],
|
|
80
74
|
|
|
81
75
|
parameters: Type.Object({
|
|
82
76
|
query: Type.String({ description: "The search query" }),
|
|
@@ -128,6 +122,13 @@ export const webSearchTool = defineTool({
|
|
|
128
122
|
},
|
|
129
123
|
),
|
|
130
124
|
),
|
|
125
|
+
pageno: Type.Optional(
|
|
126
|
+
Type.Number({
|
|
127
|
+
description:
|
|
128
|
+
"Page number, 1-indexed (default: 1). Combine with count to page through results.",
|
|
129
|
+
minimum: 1,
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
131
132
|
engines: Type.Optional(
|
|
132
133
|
Type.String({
|
|
133
134
|
description:
|
|
@@ -140,6 +141,7 @@ export const webSearchTool = defineTool({
|
|
|
140
141
|
const {
|
|
141
142
|
query,
|
|
142
143
|
count = 5,
|
|
144
|
+
pageno = 1,
|
|
143
145
|
timeout: userTimeout,
|
|
144
146
|
language = "",
|
|
145
147
|
safesearch = "0",
|
|
@@ -174,6 +176,7 @@ export const webSearchTool = defineTool({
|
|
|
174
176
|
// ── Build URL ──
|
|
175
177
|
const url = buildSearchUrl(searxngUrl, query, {
|
|
176
178
|
count,
|
|
179
|
+
pageno,
|
|
177
180
|
language,
|
|
178
181
|
safesearch,
|
|
179
182
|
time_range,
|
|
@@ -359,6 +362,7 @@ export const webSearchTool = defineTool({
|
|
|
359
362
|
details: {
|
|
360
363
|
resultCount: results.length,
|
|
361
364
|
query,
|
|
365
|
+
pageno,
|
|
362
366
|
timeout: timeoutSeconds,
|
|
363
367
|
results: results.map((r) => ({
|
|
364
368
|
title: r.title,
|
|
@@ -392,6 +396,8 @@ export const webSearchTool = defineTool({
|
|
|
392
396
|
const parts: string[] = [theme.fg("toolTitle", theme.bold("web-search "))];
|
|
393
397
|
parts.push(theme.fg("accent", `"${args.query}"`));
|
|
394
398
|
if (args.count) parts.push(theme.fg("dim", `count=${args.count}`));
|
|
399
|
+
if (args.pageno && args.pageno > 1)
|
|
400
|
+
parts.push(theme.fg("dim", `p${args.pageno}`));
|
|
395
401
|
if (args.category) parts.push(theme.fg("dim", `cat:${args.category}`));
|
|
396
402
|
if (args.time_range) parts.push(theme.fg("dim", `time:${args.time_range}`));
|
|
397
403
|
return new Text(parts.join(" "), 0, 0);
|