pi-lean-dimension 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.
Files changed (130) hide show
  1. package/README.md +96 -36
  2. package/node_modules/pi-lean-portal/AGENTS.md +146 -4
  3. package/node_modules/pi-lean-portal/README.md +112 -50
  4. package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
  5. package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
  6. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
  7. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
  8. package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
  9. package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
  10. package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
  11. package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
  12. package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
  13. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
  15. package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
  16. package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
  17. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  18. package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
  19. package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
  20. package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
  21. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
  22. package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
  23. package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
  24. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
  25. package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
  26. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
  27. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
  28. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
  29. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
  30. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
  31. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
  32. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
  33. package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
  34. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
  35. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
  36. package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
  37. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  38. package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
  39. package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
  40. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  41. package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
  42. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
  43. package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
  44. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  45. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
  48. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
  49. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
  50. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
  51. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  52. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  53. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
  54. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
  55. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
  56. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  57. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  58. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  59. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  60. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  61. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  62. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  63. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  64. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  65. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  66. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  67. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  68. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
  69. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  70. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  72. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
  84. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
  85. package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
  87. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
  88. package/node_modules/pi-lean-portal/contributed/README.md +304 -0
  89. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
  90. package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
  91. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
  92. package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
  93. package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
  94. package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
  95. package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
  96. package/node_modules/pi-lean-portal/core/router.ts +59 -98
  97. package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
  98. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
  99. package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
  100. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
  101. package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
  102. package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
  103. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
  104. package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
  105. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
  106. package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  107. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
  108. package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
  109. package/node_modules/pi-lean-portal/index.ts +42 -7
  110. package/node_modules/pi-lean-portal/package.json +8 -3
  111. package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
  112. package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
  113. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
  114. package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
  115. package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
  116. package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
  117. package/node_modules/pi-lean-search/README.md +6 -2
  118. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
  119. package/node_modules/pi-lean-search/index.ts +4 -21
  120. package/node_modules/pi-lean-search/package.json +2 -2
  121. package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
  122. package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
  123. package/package.json +4 -4
  124. package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
  125. package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
  126. package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
  127. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
  128. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
  129. package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
  130. package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
@@ -1,12 +1,17 @@
1
- import { verifyShipManifest } from "./verify-ship-manifest.js";
1
+ import { verifyShipManifest } from "./core/shared/ship-manifest.js";
2
2
  import { describe, expect, it } from "vitest";
3
3
 
4
4
  describe("publish manifest", () => {
5
5
  it("`package.json` `files` array covers every production .ts module across the tree", () => {
6
- expect(verifyShipManifest(import.meta.url).missing).toEqual([]);
6
+ expect(
7
+ verifyShipManifest(import.meta.url, { skipDirs: ["contributed"] })
8
+ .missing,
9
+ ).toEqual([]);
7
10
  });
8
11
 
9
12
  it("every `files` entry points at something on disk — a stale entry ships nothing", () => {
10
- expect(verifyShipManifest(import.meta.url).stale).toEqual([]);
13
+ expect(
14
+ verifyShipManifest(import.meta.url, { skipDirs: ["contributed"] }).stale,
15
+ ).toEqual([]);
11
16
  });
12
17
  });
@@ -7,6 +7,7 @@ import { Type } from "@earendil-works/pi-ai";
7
7
  import { Text } from "@earendil-works/pi-tui";
8
8
  import * as router from "../core/router.js";
9
9
  import { taskId } from "../core/shared/task-id.js";
10
+ import { renderExpandedText } from "./utils.js";
10
11
 
11
12
  export const browserInspectTool = defineTool({
12
13
  name: "browser-inspect",
@@ -124,13 +125,8 @@ export const browserInspectTool = defineTool({
124
125
  const staleness = d?.stalenessWarning
125
126
  ? ` ${theme.fg("warning", "(stale)")}`
126
127
  : "";
127
- const preview = content.replace(/\n{3,}/g, "\n\n").slice(0, 100);
128
128
  let text = theme.fg("accent", `🔍 ${content.length} chars`) + staleness;
129
- if (preview) {
130
- text += `\n${theme.fg("dim", preview)}`;
131
- if (content.length > 100)
132
- text += `\n${theme.fg("muted", `… ${content.length - 100} more chars`)}`;
133
- }
129
+ text = renderExpandedText(text, theme, content, 100);
134
130
  return new Text(text, 0, 0);
135
131
  },
136
132
  });
@@ -12,7 +12,11 @@ import { getConversationDefaultProfile } from "../browser-toggle.js";
12
12
  import { sessionManager } from "../core/shared/session-manager.js";
13
13
  import { removeSnapshotFiles } from "../core/shared/snapshot-cache.js";
14
14
  import { taskId } from "../core/shared/task-id.js";
15
- import { updateFooterStatus, profileLine } from "./utils.js";
15
+ import {
16
+ updateFooterStatus,
17
+ profileLine,
18
+ renderExpandedText,
19
+ } from "./utils.js";
16
20
 
17
21
  export const browserNavigateTool = defineTool({
18
22
  name: "browser-navigate",
@@ -242,10 +246,8 @@ export const browserNavigateTool = defineTool({
242
246
 
243
247
  const content = (result.content?.[0] as any)?.text ?? "";
244
248
  if (expanded) {
245
- const preview = content.replace(/\n{3,}/g, "\n\n").slice(0, 500);
246
- text += `\n\n${theme.fg("dim", preview)}`;
247
- if (content.length > 500)
248
- text += `\n${theme.fg("muted", `… ${content.length - 500} more chars`)}`;
249
+ text += "\n";
250
+ text = renderExpandedText(text, theme, content, 500);
249
251
  } else {
250
252
  text += `\n${theme.fg("muted", `${content.length} chars (expand)`)}`;
251
253
  }
@@ -7,7 +7,7 @@ import { Type } from "@earendil-works/pi-ai";
7
7
  import { Text } from "@earendil-works/pi-tui";
8
8
  import * as router from "../core/router.js";
9
9
  import { taskId } from "../core/shared/task-id.js";
10
- import { updateFooterStatus } from "./utils.js";
10
+ import { updateFooterStatus, renderExpandedText } from "./utils.js";
11
11
 
12
12
  export const browserSnapshotTool = defineTool({
13
13
  name: "browser-snapshot",
@@ -72,12 +72,9 @@ export const browserSnapshotTool = defineTool({
72
72
  const content = (result.content?.[0] as any)?.text ?? "";
73
73
  const isFull = !!(d?.full as boolean);
74
74
  if (expanded) {
75
- const preview = content.slice(0, 400);
76
75
  let text = theme.fg("accent", `📋 ${ec} elements`);
77
76
  text += isFull ? "" : theme.fg("dim", " (compact)");
78
- text += `\n${theme.fg("dim", preview)}`;
79
- if (content.length > 400)
80
- text += `\n${theme.fg("muted", `… ${content.length - 400} more chars`)}`;
77
+ text = renderExpandedText(text, theme, content, 400);
81
78
  return new Text(text, 0, 0);
82
79
  }
83
80
  const label = isFull ? " (full)" : " (compact)";
@@ -5,7 +5,7 @@
5
5
  * Includes taskId resolution, status bar updates, and profile line formatting.
6
6
  */
7
7
 
8
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import type { ExtensionAPI, ThemeColor } from "@earendil-works/pi-coding-agent";
9
9
  import { getToggleState, getLearnState } from "../browser-toggle.js";
10
10
  import { sessionManager } from "../core/shared/session-manager.js";
11
11
  import { taskId } from "../core/shared/task-id.js";
@@ -21,7 +21,7 @@ import { taskId } from "../core/shared/task-id.js";
21
21
  let _lastCtx: {
22
22
  ui: {
23
23
  setStatus: (key: string, label: string) => void;
24
- theme: { fg: (c: string, t: string) => string };
24
+ theme: { fg: (c: ThemeColor, t: string) => string };
25
25
  };
26
26
  } | null = null;
27
27
 
@@ -31,7 +31,7 @@ let _lastCtx: {
31
31
  export function updateFooterStatus(ctx: {
32
32
  ui: {
33
33
  setStatus: (key: string, label: string) => void;
34
- theme: { fg: (c: string, t: string) => string };
34
+ theme: { fg: (c: ThemeColor, t: string) => string };
35
35
  };
36
36
  }): void {
37
37
  _lastCtx = ctx;
@@ -109,7 +109,7 @@ export async function executeInteractionTool<
109
109
  ctx: {
110
110
  ui: {
111
111
  setStatus: (key: string, label: string) => void;
112
- theme: { fg: (c: string, t: string) => string };
112
+ theme: { fg: (c: ThemeColor, t: string) => string };
113
113
  };
114
114
  sessionManager?: { getSessionId?(): string };
115
115
  },
@@ -154,4 +154,22 @@ export async function executeInteractionTool<
154
154
  };
155
155
  }
156
156
 
157
+ /**
158
+ * Append a dim-styled content preview to an in-progress result string,
159
+ * with a "more chars" suffix when the content exceeds the given limit.
160
+ */
161
+ export function renderExpandedText(
162
+ text: string,
163
+ theme: { fg: (c: ThemeColor, t: string) => string },
164
+ content: string,
165
+ limit: number,
166
+ ): string {
167
+ const preview = content.replace(/\n{3,}/g, "\n\n").slice(0, limit);
168
+ if (!preview) return text;
169
+ text += `\n${theme.fg("dim", preview)}`;
170
+ if (content.length > limit)
171
+ text += `\n${theme.fg("muted", `… ${content.length - limit} more chars`)}`;
172
+ return text;
173
+ }
174
+
157
175
  export type { ExtensionAPI };
@@ -7,6 +7,7 @@ import { Type } from "@earendil-works/pi-ai";
7
7
  import { Text } from "@earendil-works/pi-tui";
8
8
  import { webFetch } from "../core/fetch-backend.js";
9
9
  import { taskId } from "../core/shared/task-id.js";
10
+ import { renderExpandedText } from "./utils.js";
10
11
 
11
12
  export const webFetchTool = defineTool({
12
13
  name: "web-fetch",
@@ -135,10 +136,8 @@ export const webFetchTool = defineTool({
135
136
 
136
137
  const content = (result.content?.[0] as any)?.text ?? "";
137
138
  if (expanded) {
138
- const preview = content.replace(/\n{3,}/g, "\n\n").slice(0, 500);
139
- text += `\n\n${theme.fg("dim", preview)}`;
140
- if (content.length > 500)
141
- text += `\n${theme.fg("muted", `… ${content.length - 500} more chars`)}`;
139
+ text += "\n";
140
+ text = renderExpandedText(text, theme, content, 500);
142
141
  } else {
143
142
  text += `\n${theme.fg("muted", `${content.length} chars (expand)`)}`;
144
143
  }
@@ -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
@@ -128,6 +128,17 @@ describe("webSearchTool", () => {
128
128
  expect(schema).toBeDefined();
129
129
  });
130
130
 
131
+ it("defines pageno parameter with minimum of 1", () => {
132
+ const schema = webSearchTool.parameters;
133
+ expect(schema).toBeDefined();
134
+ const pageno = (
135
+ schema as Record<string, unknown> & {
136
+ properties?: Record<string, unknown>;
137
+ }
138
+ )?.properties?.pageno;
139
+ expect(pageno).toBeDefined();
140
+ });
141
+
131
142
  it("has execute function", () => {
132
143
  expect(typeof webSearchTool.execute).toBe("function");
133
144
  });
@@ -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
  /**
@@ -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);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-lean-dimension",
3
- "version": "0.1.0",
4
- "description": "Pi extension. The full pi web-tools suite — interactive browsing (pi-lean-portal) and SearXNG search (pi-lean-search), unified under /web.",
3
+ "version": "0.2.0",
4
+ "description": "Pi extension. The full Pi web-tools suite in one install — interactive browsing (pi-lean-portal) plus SearXNG search (pi-lean-search), unified under a /web toggle that removes the tools from context when switched off.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",
@@ -32,8 +32,8 @@
32
32
  ]
33
33
  },
34
34
  "dependencies": {
35
- "pi-lean-portal": "^0.1.0",
36
- "pi-lean-search": "^0.1.0"
35
+ "pi-lean-portal": "^0.2.0",
36
+ "pi-lean-search": "^0.2.0"
37
37
  },
38
38
  "bundledDependencies": [
39
39
  "pi-lean-portal",