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 CHANGED
@@ -1,6 +1,10 @@
1
1
  # pi-lean-search
2
2
 
3
- > SearXNG search tool for Pi. Part of the [pi-lean-dimension](https://github.com/coreyryanhanson/pi-lean-dimension) web-tools suite.
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
- ? combineAbortSignals(signal, controller.signal)
48
- : (controller.signal as AbortSignal);
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
- ? combineAbortSignals(signal, controller.signal)
79
- : (controller.signal as AbortSignal);
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.1.0",
4
- "description": "Pi extension. SearXNG search tool for Pi. Pairs with pi-lean-portal's /web toggle (soft peer — search-only installs are valid).",
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",
@@ -1,96 +1,10 @@
1
1
  /**
2
- * Ship-manifest verification helper.
2
+ * Ship-manifest verification helper (thin re-export).
3
3
  *
4
- * Walks a package directory for production `.ts` files and compares them to
5
- * the `files` array in `package.json`. Used by `ship-manifest.test.ts` files
6
- * across the monorepo so every published package can prove its npm tarball
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
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
13
- import { dirname, relative, resolve } from "node:path";
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";
@@ -1,16 +1,6 @@
1
1
  /**
2
- * web-search tool definition for pi-lean-search.
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);