pi-chrome-search 0.1.1 → 2.1.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 (64) hide show
  1. package/README.md +52 -76
  2. package/assets/anchor.html +2 -2
  3. package/package.json +7 -3
  4. package/src/cdp/bytes.ts +14 -14
  5. package/src/cdp/client.ts +687 -0
  6. package/src/cdp/page.ts +716 -0
  7. package/src/cdp/readiness.ts +119 -0
  8. package/src/cdp/shared.ts +70 -0
  9. package/src/cdp/types.ts +30 -1
  10. package/src/cli/doctor.ts +14 -17
  11. package/src/commands/focus.ts +2 -2
  12. package/src/config/defaults.ts +177 -0
  13. package/src/config/doc.ts +183 -0
  14. package/src/config/env.ts +58 -0
  15. package/src/config/index.ts +10 -0
  16. package/src/config/load.ts +40 -0
  17. package/src/config/schema.ts +266 -0
  18. package/src/engines/avito.ts +552 -0
  19. package/src/engines/duckduckgo.ts +7 -10
  20. package/src/engines/google.ts +98 -23
  21. package/src/engines/images.ts +1 -1
  22. package/src/engines/index.ts +17 -16
  23. package/src/engines/types.ts +90 -8
  24. package/src/engines/yandex.ts +40 -21
  25. package/src/extract/claims.ts +1 -1
  26. package/src/extract/compact.ts +88 -6
  27. package/src/extract/facts.ts +39 -0
  28. package/src/extract/goto.ts +124 -2
  29. package/src/extract/text.ts +43 -0
  30. package/src/index.ts +52 -80
  31. package/src/runtime/artifacts.ts +6 -11
  32. package/src/runtime/attention.ts +2 -2
  33. package/src/runtime/budget.ts +142 -0
  34. package/src/runtime/cache.ts +1 -3
  35. package/src/runtime/doctor.ts +43 -46
  36. package/src/runtime/error-codes.ts +19 -8
  37. package/src/runtime/errors-doc.ts +45 -0
  38. package/src/runtime/limiter.ts +38 -226
  39. package/src/runtime/log.ts +2 -2
  40. package/src/runtime/panel.ts +32 -6
  41. package/src/runtime/types.ts +2 -17
  42. package/src/{cdp/discovery.ts → session/discover.ts} +3 -3
  43. package/src/session/index.ts +34 -0
  44. package/src/session/lock.ts +346 -0
  45. package/src/{cdp → session}/page-lock.ts +12 -13
  46. package/src/session/session.ts +684 -0
  47. package/src/session/tabs.ts +277 -0
  48. package/src/tools/image_search.ts +21 -7
  49. package/src/tools/index.ts +18 -7
  50. package/src/tools/surface.ts +97 -0
  51. package/src/tools/types.ts +2 -5
  52. package/src/tools/web_doctor.ts +2 -2
  53. package/src/tools/web_read.ts +159 -114
  54. package/src/tools/web_research.ts +33 -207
  55. package/src/tools/web_search.ts +246 -425
  56. package/src/version.ts +12 -3
  57. package/src/cdp/browser.ts +0 -1286
  58. package/src/cdp/session.ts +0 -1446
  59. package/src/config.ts +0 -651
  60. package/src/reflex/classifier.ts +0 -1578
  61. package/src/reflex/index.ts +0 -172
  62. package/src/reflex/policy.ts +0 -792
  63. package/src/reflex/rules.ts +0 -557
  64. package/src/reflex/types.ts +0 -117
package/README.md CHANGED
@@ -1,90 +1,66 @@
1
1
  # pi-chrome-search
2
- Free, keyless web search for [pi](https://pi.dev) through **your own, visible Chrome** — same profile,
3
- same cookies, same IP — returning compact Markdown from Google, DuckDuckGo and Yandex with each engine's
4
- native AI summary (AI Overview, Search Assist, "Быстрый ответ Алисы AI"), plus page reading and image
5
- search. The window staying visible is intentional: you can watch the search and solve a captcha yourself.
6
- It is not an automation browser: raw CDP, one owned window, one work tab per engine, ≤2500 chars of
7
- output, every failure carrying a code, a stage and a hint. No paid search APIs, no keys, no captcha
8
- solving, no fingerprint spoofing, no headless mode.
2
+
3
+ [![npm](https://img.shields.io/npm/v/pi-chrome-search)](https://www.npmjs.com/package/pi-chrome-search)
4
+ [![downloads](https://img.shields.io/npm/dm/pi-chrome-search)](https://www.npmjs.com/package/pi-chrome-search)
5
+ [![pi](https://img.shields.io/badge/pi-package-6f42c1)](https://pi.dev/packages)
6
+ [![ci](https://github.com/sh2nk/pi-chrome-search/actions/workflows/ci.yml/badge.svg)](https://github.com/sh2nk/pi-chrome-search/actions/workflows/ci.yml)
7
+ [![license](https://img.shields.io/npm/l/pi-chrome-search)](LICENSE)
8
+ [![node](https://img.shields.io/badge/node-%3E%3D26-brightgreen)](package.json)
9
+
10
+ Web search and page reading for [pi](https://pi.dev), through your own visible Chrome. Same profile,
11
+ cookies and IP. Google, DuckDuckGo and Yandex return compact Markdown and their native AI summary when
12
+ there is one; Avito listings (price, location, age) are available opt-in. No keys, no paid APIs, no
13
+ headless mode, no fingerprint spoofing. Node 26+, Chrome 136+.
9
14
 
10
15
  ## Install
11
- Node ≥ 26 (native TypeScript, no build step), Chrome 136+ (tested on 157, Wayland), pi, a headed desktop
12
- session. Then `/web-doctor` inside pi must report a live CDP endpoint and `navigator.webdriver === false`.
13
16
 
14
17
  ```bash
15
- npm install # ws + typescript (dev); no build step
16
- pi install . # register this checkout as a pi package
17
- pi -e ./src/index.ts # …or load it for one throwaway session
18
+ pi install npm:pi-chrome-search
18
19
  ```
19
20
 
20
- ## Enable CDP (once per Chrome session)
21
- Chrome 136+ ignores `--remote-debugging-port` for the default profile; debugging is enabled from inside
22
- the browser and needs an explicit **Allow** click.
21
+ Chrome 136+ enables debugging only after an explicit click:
23
22
 
24
- 1. Open `chrome://inspect/#remote-debugging` in the Chrome window the agent should use.
25
- 2. Tick **Discover network targets**, then click **Allow** on the prompt.
26
- 3. Check the endpoint file (line 1 = port, line 2 = `/devtools/browser/<uuid>`):
27
- `cat ~/.config/google-chrome/DevToolsActivePort`
23
+ 1. Open `chrome://inspect/#remote-debugging`.
24
+ 2. Tick **Discover network targets**, then click **Allow**.
28
25
 
29
- `curl 127.0.0.1:9222/json/version` may return nothing in this mode — expected; the file is authoritative.
30
- Discovery order: `PI_CHROME_CDP_WS` → `PI_CHROME_CDP_PORTS` → `DevToolsActivePort` → `cdp.ports`. The
31
- extension never launches, restarts or kills Chrome, never edits your profile, never closes foreign tabs.
26
+ The window stays visible. The extension attaches to it and never launches, restarts or closes Chrome.
32
27
 
33
28
  ## Tools
34
- `web_search` is `direct`; everything else is `deferred` (tool search + `codemode`, not paid per prompt).
35
- All descriptions together stay under 900 characters.
36
29
 
30
+ `web_search` and `web_read` are direct. The other three sit behind `tool_search`.
31
+
32
+ <!-- tools:start -->
37
33
  | Tool | Exposure | Arguments |
38
34
  |---|---|---|
39
- | `web_search` | direct | `q`; `n` 1–10 (5); `engines` `auto`\|id\|list; `summary` (true); `freshness` `d\|w\|m\|y`; `page`; `format` `md\|json` |
40
- | `web_read` | deferred | `url` (http/https); `offset`; `maxChars` (6000, ceiling 20000); `format` `md\|text` |
41
- | `image_search` | deferred | `q`; `n` ≤12 (4); `engine` `google\|yandex`; `attach` (≤4 thumbnails) |
42
- | `web_research` | deferred | `q`; `budget` `quick` (1 search + 2 reads) \| `normal` (2+4) \| `deep` (4+8) |
43
- | `web_doctor` | deferred | `probe` (true), `fix` (false: reconnect, close owned tabs, prune artifacts) |
44
-
45
- Output is capped at 2500 chars (summary ≤1200, snippets ≤140). The AI block has three states: `ai:yes`
46
- only for ≥200 chars of prose, `ai:lead` (block present, no answer — never rendered as one), `ai:no`.
47
-
48
- ## Configuration
49
- `~/.pi/agent/web-search.json` (or `$PI_CODING_AGENT_DIR/web-search.json`), optional and strict: an
50
- unknown key is `E_CONFIG`. All keys: [`docs/config.md`](docs/config.md). The six that matter most:
51
- `engines.google.enabled` (`true`, per-engine switch) · `search.n` (`5`, ≤`search.nMax` 10) ·
52
- `search.aiWaitMs` (`6000`, bounded AI-block wait) · `read.maxCharsCeiling` (`20000`) ·
53
- `window.follow` (`"work"`, which tab is raised during a stage) · `reflex.pageGate` (`true`,
54
- source-quality gate in `web_research`, rules floor in `web_read`).
55
-
56
- Env overrides: `PI_CHROME_CDP_WS`, `PI_CHROME_CDP_PORTS`, `PI_CHROME_SEARCH_PROFILE`,
57
- `PI_CHROME_SEARCH_DEBUG`, `PI_CODING_AGENT_DIR`. System 1 triage stays off (`reflex.enabled: false`):
58
- the benchmark showed no honest token win.
59
-
60
- ## When it fails
61
- | Code | Action |
62
- |---|---|
63
- | `E_CDP_UNAVAILABLE` | Open `chrome://inspect/#remote-debugging`, tick **Discover network targets**, click **Allow**; or set `PI_CHROME_CDP_WS` |
64
- | `E_CDP_AWAITING_ALLOW` | The prompt is waiting; click it — the call resumes by itself (`cdp.attachWaitMs`, 60 s) |
65
- | `E_WALL` | A captcha/consent page: solve it in the visible window, then retry. Never automated |
66
- | `E_BUSY` | Another pi session holds `~/.pi/agent/web-search.lock`: close it or wait for the heartbeat |
67
- | `E_PARSE` | A selector canary failed: open the artifact dir, compare `page.txt` with `docs/engines.md` |
68
-
69
- All 18 codes with hints: [`docs/errors.md`](docs/errors.md). `/web-doctor` (or
70
- `node src/cli/doctor.ts --json`) is the first stop; `--fix` reconnects, closes only owned tabs and prunes
71
- artifacts under `~/.pi/agent/web-search/`.
72
-
73
- ## Security and ToS
74
- Your session, your cookies: searches and reads happen in your browser as you. `web_read` can open pages
75
- you are logged into but never exfiltrates content, and it refuses loopback/private/`file:`/`data:` URLs
76
- (re-checked on redirect). No telemetry; artifacts are sanitized and git-ignored. CDP is powerful — any
77
- local process that reaches the debugging port can drive the browser; it binds loopback only and you
78
- enable it explicitly. The optional page gate sends only URL/title/snippet of ≤10 sources (never page
79
- text) to your configured classifier; `reflex.localOnly: true` keeps it local.
80
-
81
- Google and Yandex prohibit automated access in their terms: this project does not claim compliance and
82
- must not be run as a service. If an engine shows a captcha, solve it in the window or stop.
83
-
84
- ## Development
85
- `npm run lint` (typecheck + audit) · `npm test` (offline contract suite) · `npm run test:live` (opt-in,
86
- one query per engine, no retries, `PI_CHROME_LIVE=1`). Decisions: [`docs/architecture.md`](docs/architecture.md)
87
- · selectors: [`docs/engines.md`](docs/engines.md). Never launch a browser from tests; never loop probes.
88
-
89
- ## License
90
- MIT — see [`LICENSE`](LICENSE).
35
+ | `web_search` | direct | `q` string; `n?` integer 1..10; `engines?` "auto" or ("google" or "duckduckgo" or "yandex" or "avito") or list of ("google" or "duckduckgo" or "yandex" or "avito") 1..4; `summary?` boolean; `page?` integer >=1; `format?` "md" or "json" |
36
+ | `web_read` | direct | `url` string; `offset?` integer >=0; `maxChars?` integer >=1; `format?` "md" or "text" |
37
+ | `image_search` | deferred | `q` string; `n?` integer 1..12; `engine?` "google" or "yandex"; `attach?` boolean or "bytes" |
38
+ | `web_research` | deferred | `q` string; `budget?` "quick" or "normal" or "deep" |
39
+ | `web_doctor` | deferred | `probe?` boolean; `fix?` boolean |
40
+ <!-- tools:end -->
41
+
42
+ A call has one 60 second deadline. DuckDuckGo has no pagination: `page > 1` is reported as
43
+ `ignored.page`; Avito has no AI block: `summary` is reported as `ignored.summary`. Every unresolved
44
+ link stays marked `unresolved`, and card facts (price/availability/rating/location) render on their
45
+ own line after the snippet.
46
+
47
+ `engines: "auto"` only ever means Google → DuckDuckGo → Yandex. Avito is a *special* engine: it is off
48
+ by default and reachable only by naming it. To turn it on, set `engines.avito.enabled` to `true` (and
49
+ optionally `engines.avito.location`, a region slug such as `kaluga` or `all`) in
50
+ `~/.pi/agent/web-search.json` — ask your agent to do it, that is the intended path.
51
+
52
+ ## Limits
53
+
54
+ No headless browsing, automation profile, captcha solving, proxy rotation, cookie export, telemetry or
55
+ paid search API. Google and Yandex forbid automated access in their terms. If a captcha appears, solve it
56
+ in the window or stop.
57
+
58
+ ## Failures
59
+
60
+ Every failure is an `E_*` code with a stage, a retry flag and a hint you can act on. The full table is in
61
+ [errors.md](https://github.com/sh2nk/pi-chrome-search/blob/main/docs/errors.md). Start with
62
+ `/web-doctor` or `node src/cli/doctor.ts --json`.
63
+
64
+ Config reference: [config.md](https://github.com/sh2nk/pi-chrome-search/blob/main/docs/config.md).
65
+
66
+ MIT.
@@ -83,7 +83,7 @@
83
83
  </section>
84
84
 
85
85
  <section class="card">
86
- <h2>Daily counters</h2>
86
+ <h2>Session counters</h2>
87
87
  <table>
88
88
  <tbody>
89
89
  <tr><td>google</td><td class="num" id="count-google">—</td></tr>
@@ -135,7 +135,7 @@
135
135
  var cell = counters[entry.engine];
136
136
  if (!cell) continue;
137
137
  seen[entry.engine] = true;
138
- cell.textContent = String(entry.today) + " / " + String(entry.cap);
138
+ cell.textContent = String(entry.today);
139
139
  }
140
140
  for (var name in counters) {
141
141
  if (!seen[name] && counters[name]) counters[name].textContent = "—";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-chrome-search",
3
- "version": "0.1.1",
4
- "description": "Free, keyless web search through the user's own visible Chrome (Google, DuckDuckGo, Yandex) for pi",
3
+ "version": "2.1.0",
4
+ "description": "Free, keyless web search through the user's own visible Chrome (Google, DuckDuckGo, Yandex, and opt-in Avito listings) for pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "pi": {
@@ -18,6 +18,9 @@
18
18
  "test": "node --test --test-timeout=15000 \"tests/unit/**/*.test.ts\"",
19
19
  "test:live": "PI_CHROME_LIVE=1 node --test \"tests/live/**/*.test.ts\"",
20
20
  "audit": "node scripts/audit.mjs",
21
+ "gen-config-doc": "node scripts/gen-config-doc.mjs",
22
+ "gen-errors-doc": "node scripts/gen-errors-doc.mjs",
23
+ "gen-readme": "node scripts/gen-readme.mjs",
21
24
  "release": "node scripts/release.mjs"
22
25
  },
23
26
  "peerDependencies": {
@@ -42,7 +45,8 @@
42
45
  "web-search",
43
46
  "google",
44
47
  "duckduckgo",
45
- "yandex"
48
+ "yandex",
49
+ "avito"
46
50
  ],
47
51
  "files": [
48
52
  "index.ts",
package/src/cdp/bytes.ts CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { isWebSearchError } from "../runtime/error-codes.ts";
26
26
  import { fail } from "../runtime/errors.ts";
27
- import type { CdpSession } from "./session.ts";
27
+ import type { CdpClient } from "./types.ts";
28
28
  import type { FetchedBytes, FetchBytesOptions } from "./types.ts";
29
29
 
30
30
  export const DEFAULT_FETCH_TIMEOUT_MS = 20_000;
@@ -95,7 +95,7 @@ async function withDeadline<T>(work: Promise<T>, timeoutMs: number, url: string)
95
95
  * is the final hop, and the tool re-runs the guard on it before using the body.
96
96
  */
97
97
  export async function captureBytes(
98
- session: CdpSession,
98
+ client: CdpClient,
99
99
  url: string,
100
100
  options: FetchBytesOptions = {},
101
101
  ): Promise<FetchedBytes> {
@@ -113,15 +113,15 @@ export async function captureBytes(
113
113
  try {
114
114
  // `background: true` keeps the user's foreground tab where it is; Chrome
115
115
  // 132+ accepts it and older builds ignore the flag rather than failing.
116
- targetId = await session.createTarget("about:blank", { background: true });
117
- await session.attach(targetId);
118
- sessionId = session.sessionIdFor(targetId);
116
+ targetId = await client.createTarget("about:blank", { background: true });
117
+ await client.attach(targetId);
118
+ sessionId = client.sessionIdFor(targetId);
119
119
  if (sessionId === undefined) {
120
120
  throw fail("E_CDP_DISCONNECTED", "connect", { message: "the capture target is not attached" });
121
121
  }
122
122
  const captureSessionId = sessionId;
123
123
 
124
- await session.send(
124
+ await client.send(
125
125
  "Fetch.enable",
126
126
  { patterns: [{ urlPattern: "*", requestStage: "Response" }] },
127
127
  captureSessionId,
@@ -157,7 +157,7 @@ export async function captureBytes(
157
157
 
158
158
  // Redirects are part of the chain; keep following until the body hop.
159
159
  if (status >= 300 && status < 400) {
160
- void session
160
+ void client
161
161
  .send("Fetch.continueResponse", { requestId }, captureSessionId)
162
162
  .catch(() => undefined);
163
163
  return;
@@ -165,7 +165,7 @@ export async function captureBytes(
165
165
 
166
166
  const declared = contentLength(headers);
167
167
  if (declared !== undefined && declared > maxBytes) {
168
- void session.send("Fetch.failRequest", { requestId, errorReason: "Aborted" }, captureSessionId).catch(
168
+ void client.send("Fetch.failRequest", { requestId, errorReason: "Aborted" }, captureSessionId).catch(
169
169
  () => undefined,
170
170
  );
171
171
  finish(() =>
@@ -185,7 +185,7 @@ export async function captureBytes(
185
185
 
186
186
  void (async () => {
187
187
  try {
188
- const body = await session.send<Record<string, unknown>>(
188
+ const body = await client.send<Record<string, unknown>>(
189
189
  "Fetch.getResponseBody",
190
190
  { requestId },
191
191
  captureSessionId,
@@ -209,18 +209,18 @@ export async function captureBytes(
209
209
  } catch (error) {
210
210
  finish(() => reject(error));
211
211
  } finally {
212
- void session
212
+ void client
213
213
  .send("Fetch.failRequest", { requestId, errorReason: "Aborted" }, captureSessionId)
214
214
  .catch(() => undefined);
215
215
  }
216
216
  })();
217
217
  };
218
218
 
219
- onEvent = session.on("Fetch.requestPaused", handler);
219
+ onEvent = client.on("Fetch.requestPaused", handler);
220
220
  // The request is opened by the document, not by `Page.navigate`: the
221
221
  // capture target never shows a PDF viewer, so there is no "load" to
222
222
  // await and no viewer document to mistake for the payload.
223
- void session
223
+ void client
224
224
  .send(
225
225
  "Runtime.evaluate",
226
226
  {
@@ -248,10 +248,10 @@ export async function captureBytes(
248
248
  });
249
249
  } finally {
250
250
  if (sessionId !== undefined) {
251
- await session.send("Fetch.disable", {}, sessionId).catch(() => undefined);
251
+ await client.send("Fetch.disable", {}, sessionId).catch(() => undefined);
252
252
  }
253
253
  if (targetId !== undefined) {
254
- await session.closeTarget(targetId).catch(() => undefined);
254
+ await client.closeTarget(targetId).catch(() => undefined);
255
255
  }
256
256
  }
257
257
  }