findweb 0.1.0 → 0.1.2

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 (3) hide show
  1. package/README.md +148 -0
  2. package/dist/index.js +104 -54
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -0,0 +1,148 @@
1
+ # findweb
2
+
3
+ Google search CLI powered by system Chrome, with programmatic ad blocking.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun install -g findweb
9
+ ```
10
+
11
+ Or run directly:
12
+
13
+ ```bash
14
+ bunx findweb "yc"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ # Single search
21
+ findweb "Y Combinator"
22
+
23
+ # Batch search
24
+ findweb "yc" "apple" "tesla" --parallel 3
25
+
26
+ # JSON output
27
+ findweb --json "react useEffect"
28
+
29
+ # Custom region and language
30
+ findweb --gl kr --lang ko "startup"
31
+
32
+ # More results
33
+ findweb -n 10 "rust async"
34
+
35
+ # Prepare a signed-in Chrome profile (reduces rate limiting)
36
+ findweb login
37
+ ```
38
+
39
+ ## First Run Behavior
40
+
41
+ `findweb` requires an initialized Google profile before it will run the first search for a given `--userDataDir`.
42
+
43
+ - If the profile is already prepared, search runs immediately.
44
+ - If the profile has not been prepared yet, `findweb` automatically opens the login flow first.
45
+ - After you sign in and close the browser window, `findweb` writes a local prepared-profile marker so future searches can start immediately.
46
+ - By default, the profile directory is `${XDG_DATA_HOME:-~/.local/share}/findweb/chrome-profile` unless you pass `--userDataDir` or set `GOOGLE_SEARCH_USER_DATA_DIR`.
47
+
48
+ In practice, the first search on a fresh profile behaves like this:
49
+
50
+ ```bash
51
+ findweb "yc"
52
+ ```
53
+
54
+ 1. detect missing prepared-profile marker
55
+ 2. open headed Chrome login flow
56
+ 3. wait for you to sign in and close the browser
57
+ 4. continue the original search
58
+
59
+ ## Options
60
+
61
+ | Option | Default | Description |
62
+ | ------------------ | -------------- | ------------------------------------ |
63
+ | `--gl <country>` | `us` | Google region hint |
64
+ | `-l, --lang` | `en` | Google UI language |
65
+ | `-n, --num` | `3` | Results per query |
66
+ | `--parallel` | `4` | Batch tab concurrency |
67
+ | `--userDataDir` | auto-detected | Chrome profile directory |
68
+ | `--headed` | `false` | Show the Chrome window |
69
+ | `--json` | `false` | Print output as JSON |
70
+
71
+ ## How It Works
72
+
73
+ 1. Launches system Chrome (`/Applications/Google Chrome.app`) with a free debugging port
74
+ 2. Connects via CDP using puppeteer-core
75
+ 3. Loads the [Ghostery adblocker](https://github.com/ghostery/adblocker) engine programmatically on each page
76
+ 4. Navigates to Google, submits the query through DOM manipulation, and extracts results from the rendered page
77
+ 5. Returns results as plain text or JSON, then closes Chrome
78
+
79
+ No Chromium download. No browser extension. No user confirmation.
80
+
81
+ ## Batch Mode
82
+
83
+ Pass multiple quoted queries as positional arguments. Each query opens a separate tab in the same browser instance and profile.
84
+
85
+ ```bash
86
+ findweb "yc" "apple" "tesla"
87
+ ```
88
+
89
+ Results are returned in input order. Concurrency is controlled by `--parallel` (default: 4).
90
+
91
+ ## Login
92
+
93
+ Google rate-limits unauthenticated or fresh-profile searches. `findweb` now enforces an interactive login before the first search on a new profile.
94
+
95
+ You can trigger that ahead of time with:
96
+
97
+ ```bash
98
+ findweb login
99
+ ```
100
+
101
+ This opens a visible Chrome window with the Google sign-in page. After signing in, close the browser. The session is saved to the profile directory, and `findweb` records that the profile is ready for future searches.
102
+
103
+ ## Output
104
+
105
+ ### Plain text (default)
106
+
107
+ ```
108
+ 1. Y Combinator
109
+ https://www.ycombinator.com/
110
+ Y Combinator created a new model for funding early stage startups.
111
+
112
+ 2. Y Combinator - Wikipedia
113
+ https://en.wikipedia.org/wiki/Y_Combinator
114
+ ```
115
+
116
+ ### JSON (`--json`)
117
+
118
+ ```json
119
+ [
120
+ {
121
+ "title": "Y Combinator",
122
+ "url": "https://www.ycombinator.com/",
123
+ "snippet": "Y Combinator created a new model for funding early stage startups."
124
+ }
125
+ ]
126
+ ```
127
+
128
+ ## Requirements
129
+
130
+ - macOS
131
+ - System Chrome (`/Applications/Google Chrome.app`)
132
+ - [Bun](https://bun.sh) >= 1.3.11
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ git clone https://github.com/ysm-dev/findweb.git
138
+ cd findweb
139
+ bun install
140
+ bun run check # tsgo typecheck
141
+ bun run test # unit tests
142
+ bun run dev # run from source
143
+ bun run build # bundle to dist/
144
+ ```
145
+
146
+ ## License
147
+
148
+ MIT
package/dist/index.js CHANGED
@@ -53157,7 +53157,7 @@ var require_ffi_WASM_RELEASE_SYNC = __commonJS((exports) => {
53157
53157
 
53158
53158
  // node_modules/@tootallnate/quickjs-emscripten/dist/generated/emscripten-module.WASM_RELEASE_SYNC.js
53159
53159
  var require_emscripten_module_WASM_RELEASE_SYNC = __commonJS((exports, module) => {
53160
- var __dirname = "/Users/chris/git/ysm/findweb/node_modules/@tootallnate/quickjs-emscripten/dist/generated", __filename = "/Users/chris/git/ysm/findweb/node_modules/@tootallnate/quickjs-emscripten/dist/generated/emscripten-module.WASM_RELEASE_SYNC.js";
53160
+ var __dirname = "/home/runner/work/findweb/findweb/node_modules/@tootallnate/quickjs-emscripten/dist/generated", __filename = "/home/runner/work/findweb/findweb/node_modules/@tootallnate/quickjs-emscripten/dist/generated/emscripten-module.WASM_RELEASE_SYNC.js";
53161
53161
  var QuickJSRaw = (() => {
53162
53162
  var _scriptDir = typeof document !== "undefined" && document.currentScript ? document.currentScript.src : undefined;
53163
53163
  if (typeof __filename !== "undefined")
@@ -66931,13 +66931,11 @@ async function runMain(cmd, opts = {}) {
66931
66931
  }
66932
66932
  }
66933
66933
 
66934
- // src/cli/commands/login.ts
66935
- import fs7 from "fs/promises";
66936
-
66937
66934
  // src/search/browser.ts
66938
- import { existsSync as existsSync3 } from "fs";
66939
66935
  import { spawn as spawn3 } from "child_process";
66940
66936
  import http2 from "http";
66937
+ import os9 from "os";
66938
+ import path12 from "path";
66941
66939
 
66942
66940
  // node_modules/puppeteer-core/lib/esm/puppeteer/api/api.js
66943
66941
  init_Browser();
@@ -76310,13 +76308,19 @@ async function closeSearchBrowser(activeBrowser) {
76310
76308
  activeBrowser.chromeProcess.kill("SIGTERM");
76311
76309
  }
76312
76310
  }
76311
+ function defaultXdgDataHome() {
76312
+ const configured = process.env.XDG_DATA_HOME;
76313
+ if (configured && path12.isAbsolute(configured)) {
76314
+ return configured;
76315
+ }
76316
+ return path12.join(os9.homedir(), ".local", "share");
76317
+ }
76313
76318
  function defaultUserDataDir() {
76314
76319
  const configured = process.env.GOOGLE_SEARCH_USER_DATA_DIR;
76315
76320
  if (configured) {
76316
76321
  return configured;
76317
76322
  }
76318
- const legacyProfile = "/tmp/gsearch-manual-login-profile";
76319
- return ["/tmp/google-search-profile", legacyProfile].find((candidate) => existsSync3(candidate)) ?? "/tmp/google-search-profile";
76323
+ return path12.join(defaultXdgDataHome(), "findweb", "chrome-profile");
76320
76324
  }
76321
76325
 
76322
76326
  // src/search/page.ts
@@ -76557,8 +76561,68 @@ async function runLoginSession(options) {
76557
76561
  });
76558
76562
  }
76559
76563
 
76564
+ // src/cli/profile.ts
76565
+ import fs7 from "fs/promises";
76566
+ import path13 from "path";
76567
+ var PROFILE_READY_MARKER = ".findweb-profile-ready";
76568
+ async function ensureProfileDir(dirPath) {
76569
+ await fs7.mkdir(dirPath, { recursive: true });
76570
+ }
76571
+ function readyMarkerPath(userDataDir) {
76572
+ return path13.join(userDataDir, PROFILE_READY_MARKER);
76573
+ }
76574
+ async function hasPreparedProfile(userDataDir) {
76575
+ try {
76576
+ await fs7.access(readyMarkerPath(userDataDir));
76577
+ return true;
76578
+ } catch {
76579
+ return false;
76580
+ }
76581
+ }
76582
+ async function markProfilePrepared(userDataDir) {
76583
+ await ensureProfileDir(userDataDir);
76584
+ await fs7.writeFile(readyMarkerPath(userDataDir), `${new Date().toISOString()}
76585
+ `, "utf8");
76586
+ }
76587
+
76588
+ // src/cli/flows/login.ts
76589
+ function printLoginInstructions(userDataDir) {
76590
+ console.log(`Login browser launched with profile: ${userDataDir}`);
76591
+ console.log("Sign in to Google to prepare this profile for future searches.");
76592
+ console.log("Close the browser window when you are done.");
76593
+ }
76594
+ async function runInteractiveLoginFlow(options) {
76595
+ await ensureProfileDir(options.userDataDir);
76596
+ const activeBrowser = await launchSearchBrowser({
76597
+ headed: true,
76598
+ lang: options.lang,
76599
+ userDataDir: options.userDataDir
76600
+ });
76601
+ printLoginInstructions(options.userDataDir);
76602
+ try {
76603
+ await runLoginSession({
76604
+ browser: activeBrowser.browser,
76605
+ gl: options.gl,
76606
+ lang: options.lang
76607
+ });
76608
+ } finally {
76609
+ await closeSearchBrowser(activeBrowser);
76610
+ }
76611
+ await markProfilePrepared(options.userDataDir);
76612
+ }
76613
+ async function ensureInteractiveLogin(options) {
76614
+ await ensureProfileDir(options.userDataDir);
76615
+ if (await hasPreparedProfile(options.userDataDir)) {
76616
+ return false;
76617
+ }
76618
+ console.log("No prepared Google profile was found for this user-data-dir.");
76619
+ console.log("A login step is required before the first search.");
76620
+ await runInteractiveLoginFlow(options);
76621
+ return true;
76622
+ }
76623
+
76560
76624
  // src/cli/schema.ts
76561
- import path12 from "path";
76625
+ import path14 from "path";
76562
76626
 
76563
76627
  // node_modules/zod/v4/classic/external.js
76564
76628
  var exports_external = {};
@@ -77325,10 +77389,10 @@ function mergeDefs(...defs) {
77325
77389
  function cloneDef(schema) {
77326
77390
  return mergeDefs(schema._zod.def);
77327
77391
  }
77328
- function getElementAtPath(obj, path12) {
77329
- if (!path12)
77392
+ function getElementAtPath(obj, path14) {
77393
+ if (!path14)
77330
77394
  return obj;
77331
- return path12.reduce((acc, key) => acc?.[key], obj);
77395
+ return path14.reduce((acc, key) => acc?.[key], obj);
77332
77396
  }
77333
77397
  function promiseAllObject(promisesObj) {
77334
77398
  const keys = Object.keys(promisesObj);
@@ -77709,11 +77773,11 @@ function aborted(x, startIndex = 0) {
77709
77773
  }
77710
77774
  return false;
77711
77775
  }
77712
- function prefixIssues(path12, issues) {
77776
+ function prefixIssues(path14, issues) {
77713
77777
  return issues.map((iss) => {
77714
77778
  var _a4;
77715
77779
  (_a4 = iss).path ?? (_a4.path = []);
77716
- iss.path.unshift(path12);
77780
+ iss.path.unshift(path14);
77717
77781
  return iss;
77718
77782
  });
77719
77783
  }
@@ -77896,7 +77960,7 @@ function formatError(error, mapper = (issue2) => issue2.message) {
77896
77960
  }
77897
77961
  function treeifyError(error, mapper = (issue2) => issue2.message) {
77898
77962
  const result = { errors: [] };
77899
- const processError = (error2, path12 = []) => {
77963
+ const processError = (error2, path14 = []) => {
77900
77964
  var _a4, _b2;
77901
77965
  for (const issue2 of error2.issues) {
77902
77966
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -77906,7 +77970,7 @@ function treeifyError(error, mapper = (issue2) => issue2.message) {
77906
77970
  } else if (issue2.code === "invalid_element") {
77907
77971
  processError({ issues: issue2.issues }, issue2.path);
77908
77972
  } else {
77909
- const fullpath = [...path12, ...issue2.path];
77973
+ const fullpath = [...path14, ...issue2.path];
77910
77974
  if (fullpath.length === 0) {
77911
77975
  result.errors.push(mapper(issue2));
77912
77976
  continue;
@@ -77938,8 +78002,8 @@ function treeifyError(error, mapper = (issue2) => issue2.message) {
77938
78002
  }
77939
78003
  function toDotPath(_path) {
77940
78004
  const segs = [];
77941
- const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
77942
- for (const seg of path12) {
78005
+ const path14 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
78006
+ for (const seg of path14) {
77943
78007
  if (typeof seg === "number")
77944
78008
  segs.push(`[${seg}]`);
77945
78009
  else if (typeof seg === "symbol")
@@ -89686,13 +89750,13 @@ function resolveRef(ref, ctx) {
89686
89750
  if (!ref.startsWith("#")) {
89687
89751
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
89688
89752
  }
89689
- const path12 = ref.slice(1).split("/").filter(Boolean);
89690
- if (path12.length === 0) {
89753
+ const path14 = ref.slice(1).split("/").filter(Boolean);
89754
+ if (path14.length === 0) {
89691
89755
  return ctx.rootSchema;
89692
89756
  }
89693
89757
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
89694
- if (path12[0] === defsKey) {
89695
- const key = path12[1];
89758
+ if (path14[0] === defsKey) {
89759
+ const key = path14[1];
89696
89760
  if (!key || !ctx.defs[key]) {
89697
89761
  throw new Error(`Reference not found: ${ref}`);
89698
89762
  }
@@ -90101,7 +90165,7 @@ var sharedSchema = exports_external.object({
90101
90165
  headed: exports_external.boolean().default(false),
90102
90166
  json: exports_external.boolean().default(false),
90103
90167
  lang: defaultedString("lang", "en"),
90104
- userDataDir: defaultedString("userDataDir", defaultUserDataDir()).transform((value) => path12.resolve(value))
90168
+ userDataDir: defaultedString("userDataDir", defaultUserDataDir()).transform((value) => path14.resolve(value))
90105
90169
  });
90106
90170
  var searchSchema = sharedSchema.extend({
90107
90171
  num: positiveInteger("num").default(3),
@@ -90145,25 +90209,6 @@ function normalizeLoginOptions(input2) {
90145
90209
  }
90146
90210
 
90147
90211
  // src/cli/commands/login.ts
90148
- async function ensureProfileDir(dirPath) {
90149
- await fs7.mkdir(dirPath, { recursive: true });
90150
- }
90151
- async function runLoginFlow(gl, lang, userDataDir) {
90152
- await ensureProfileDir(userDataDir);
90153
- const activeBrowser = await launchSearchBrowser({
90154
- headed: true,
90155
- lang,
90156
- userDataDir
90157
- });
90158
- console.log(`Login browser launched with profile: ${userDataDir}`);
90159
- console.log("Sign in to Google if you want to reuse a logged-in search profile.");
90160
- console.log("Close the browser window when you are done.");
90161
- try {
90162
- await runLoginSession({ browser: activeBrowser.browser, gl, lang });
90163
- } finally {
90164
- await closeSearchBrowser(activeBrowser);
90165
- }
90166
- }
90167
90212
  function createLoginArgs() {
90168
90213
  return {
90169
90214
  gl: {
@@ -90190,7 +90235,7 @@ async function runLogin(args) {
90190
90235
  lang: typeof args.lang === "string" ? args.lang : undefined,
90191
90236
  userDataDir: typeof args.userDataDir === "string" ? args.userDataDir : undefined
90192
90237
  });
90193
- await runLoginFlow(options.gl, options.lang, options.userDataDir);
90238
+ await runInteractiveLoginFlow(options);
90194
90239
  }
90195
90240
  function createLoginCommand(commandName = "findweb login") {
90196
90241
  return defineCommand({
@@ -90207,13 +90252,12 @@ function createLoginCommand(commandName = "findweb login") {
90207
90252
  }
90208
90253
 
90209
90254
  // src/cli/commands/search.ts
90210
- import fs9 from "fs/promises";
90211
90255
  import process4 from "process";
90212
90256
 
90213
90257
  // src/search/blocker.ts
90214
90258
  import fs8 from "fs/promises";
90215
- import os9 from "os";
90216
- import path13 from "path";
90259
+ import os10 from "os";
90260
+ import path15 from "path";
90217
90261
 
90218
90262
  // node_modules/tldts-core/dist/es6/src/domain.js
90219
90263
  function shareSameDomainSuffix(hostname3, vhost) {
@@ -99477,8 +99521,8 @@ class FilterEngine extends EventEmitter5 {
99477
99521
  if (caching === undefined) {
99478
99522
  return init();
99479
99523
  }
99480
- const { path: path13, read, write } = caching;
99481
- return read(path13).then((buffer) => this.deserialize(buffer)).catch(() => init().then((engine) => write(path13, engine.serialize()).then(() => engine)));
99524
+ const { path: path15, read, write } = caching;
99525
+ return read(path15).then((buffer) => this.deserialize(buffer)).catch(() => init().then((engine) => write(path15, engine.serialize()).then(() => engine)));
99482
99526
  }
99483
99527
  static empty(config3 = {}) {
99484
99528
  return new this({ config: config3 });
@@ -101270,13 +101314,13 @@ class PuppeteerBlocker extends FilterEngine {
101270
101314
  // src/search/blocker.ts
101271
101315
  var blockerPromise;
101272
101316
  function defaultCacheDir() {
101273
- return process.env.GOOGLE_SEARCH_CACHE_DIR ?? path13.join(os9.homedir(), ".cache", "google-search");
101317
+ return process.env.GOOGLE_SEARCH_CACHE_DIR ?? path15.join(os10.homedir(), ".cache", "google-search");
101274
101318
  }
101275
101319
  async function readCache(filePath) {
101276
101320
  return fs8.readFile(filePath);
101277
101321
  }
101278
101322
  async function writeCache(filePath, buffer) {
101279
- await fs8.mkdir(path13.dirname(filePath), { recursive: true });
101323
+ await fs8.mkdir(path15.dirname(filePath), { recursive: true });
101280
101324
  await fs8.writeFile(filePath, buffer);
101281
101325
  }
101282
101326
  async function loadBlocker() {
@@ -101285,7 +101329,7 @@ async function loadBlocker() {
101285
101329
  const cacheDir = defaultCacheDir();
101286
101330
  await fs8.mkdir(cacheDir, { recursive: true });
101287
101331
  return PuppeteerBlocker.fromPrebuiltAdsAndTracking(globalThis.fetch.bind(globalThis), {
101288
- path: path13.join(cacheDir, "ghostery-engine.bin"),
101332
+ path: path15.join(cacheDir, "ghostery-engine.bin"),
101289
101333
  read: readCache,
101290
101334
  write: writeCache
101291
101335
  });
@@ -101339,9 +101383,6 @@ function exitCodeForOutcomes(items) {
101339
101383
  }
101340
101384
 
101341
101385
  // src/cli/commands/search.ts
101342
- async function ensureProfileDir2(dirPath) {
101343
- await fs9.mkdir(dirPath, { recursive: true });
101344
- }
101345
101386
  function printResults(json2, outcomes) {
101346
101387
  if (json2) {
101347
101388
  printJsonResults(outcomes);
@@ -101402,7 +101443,16 @@ async function runSearch(args) {
101402
101443
  parallel: typeof args.parallel === "string" || typeof args.parallel === "number" ? args.parallel : undefined,
101403
101444
  userDataDir: typeof args.userDataDir === "string" ? args.userDataDir : undefined
101404
101445
  });
101405
- await ensureProfileDir2(options.userDataDir);
101446
+ await ensureProfileDir(options.userDataDir);
101447
+ const loginWasRequired = await ensureInteractiveLogin({
101448
+ gl: options.gl,
101449
+ lang: options.lang,
101450
+ userDataDir: options.userDataDir
101451
+ });
101452
+ if (loginWasRequired) {
101453
+ console.log(`Login completed. Continuing with search...
101454
+ `);
101455
+ }
101406
101456
  const blocker = await loadBlocker();
101407
101457
  const activeBrowser = await launchSearchBrowser({
101408
101458
  headed: options.headed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "findweb",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Google search CLI powered by system Chrome",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.11",