findweb 0.1.0 → 0.1.1

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 +147 -0
  2. package/dist/index.js +93 -50
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -0,0 +1,147 @@
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
+
47
+ In practice, the first search on a fresh profile behaves like this:
48
+
49
+ ```bash
50
+ findweb "yc"
51
+ ```
52
+
53
+ 1. detect missing prepared-profile marker
54
+ 2. open headed Chrome login flow
55
+ 3. wait for you to sign in and close the browser
56
+ 4. continue the original search
57
+
58
+ ## Options
59
+
60
+ | Option | Default | Description |
61
+ | ------------------ | -------------- | ------------------------------------ |
62
+ | `--gl <country>` | `us` | Google region hint |
63
+ | `-l, --lang` | `en` | Google UI language |
64
+ | `-n, --num` | `3` | Results per query |
65
+ | `--parallel` | `4` | Batch tab concurrency |
66
+ | `--userDataDir` | auto-detected | Chrome profile directory |
67
+ | `--headed` | `false` | Show the Chrome window |
68
+ | `--json` | `false` | Print output as JSON |
69
+
70
+ ## How It Works
71
+
72
+ 1. Launches system Chrome (`/Applications/Google Chrome.app`) with a free debugging port
73
+ 2. Connects via CDP using puppeteer-core
74
+ 3. Loads the [Ghostery adblocker](https://github.com/ghostery/adblocker) engine programmatically on each page
75
+ 4. Navigates to Google, submits the query through DOM manipulation, and extracts results from the rendered page
76
+ 5. Returns results as plain text or JSON, then closes Chrome
77
+
78
+ No Chromium download. No browser extension. No user confirmation.
79
+
80
+ ## Batch Mode
81
+
82
+ Pass multiple quoted queries as positional arguments. Each query opens a separate tab in the same browser instance and profile.
83
+
84
+ ```bash
85
+ findweb "yc" "apple" "tesla"
86
+ ```
87
+
88
+ Results are returned in input order. Concurrency is controlled by `--parallel` (default: 4).
89
+
90
+ ## Login
91
+
92
+ Google rate-limits unauthenticated or fresh-profile searches. `findweb` now enforces an interactive login before the first search on a new profile.
93
+
94
+ You can trigger that ahead of time with:
95
+
96
+ ```bash
97
+ findweb login
98
+ ```
99
+
100
+ 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.
101
+
102
+ ## Output
103
+
104
+ ### Plain text (default)
105
+
106
+ ```
107
+ 1. Y Combinator
108
+ https://www.ycombinator.com/
109
+ Y Combinator created a new model for funding early stage startups.
110
+
111
+ 2. Y Combinator - Wikipedia
112
+ https://en.wikipedia.org/wiki/Y_Combinator
113
+ ```
114
+
115
+ ### JSON (`--json`)
116
+
117
+ ```json
118
+ [
119
+ {
120
+ "title": "Y Combinator",
121
+ "url": "https://www.ycombinator.com/",
122
+ "snippet": "Y Combinator created a new model for funding early stage startups."
123
+ }
124
+ ]
125
+ ```
126
+
127
+ ## Requirements
128
+
129
+ - macOS
130
+ - System Chrome (`/Applications/Google Chrome.app`)
131
+ - [Bun](https://bun.sh) >= 1.3.11
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ git clone https://github.com/ysm-dev/findweb.git
137
+ cd findweb
138
+ bun install
139
+ bun run check # tsgo typecheck
140
+ bun run test # unit tests
141
+ bun run dev # run from source
142
+ bun run build # bundle to dist/
143
+ ```
144
+
145
+ ## License
146
+
147
+ 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,9 +66931,6 @@ 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
66935
  import { existsSync as existsSync3 } from "fs";
66939
66936
  import { spawn as spawn3 } from "child_process";
@@ -76557,8 +76554,68 @@ async function runLoginSession(options) {
76557
76554
  });
76558
76555
  }
76559
76556
 
76560
- // src/cli/schema.ts
76557
+ // src/cli/profile.ts
76558
+ import fs7 from "fs/promises";
76561
76559
  import path12 from "path";
76560
+ var PROFILE_READY_MARKER = ".findweb-profile-ready";
76561
+ async function ensureProfileDir(dirPath) {
76562
+ await fs7.mkdir(dirPath, { recursive: true });
76563
+ }
76564
+ function readyMarkerPath(userDataDir) {
76565
+ return path12.join(userDataDir, PROFILE_READY_MARKER);
76566
+ }
76567
+ async function hasPreparedProfile(userDataDir) {
76568
+ try {
76569
+ await fs7.access(readyMarkerPath(userDataDir));
76570
+ return true;
76571
+ } catch {
76572
+ return false;
76573
+ }
76574
+ }
76575
+ async function markProfilePrepared(userDataDir) {
76576
+ await ensureProfileDir(userDataDir);
76577
+ await fs7.writeFile(readyMarkerPath(userDataDir), `${new Date().toISOString()}
76578
+ `, "utf8");
76579
+ }
76580
+
76581
+ // src/cli/flows/login.ts
76582
+ function printLoginInstructions(userDataDir) {
76583
+ console.log(`Login browser launched with profile: ${userDataDir}`);
76584
+ console.log("Sign in to Google to prepare this profile for future searches.");
76585
+ console.log("Close the browser window when you are done.");
76586
+ }
76587
+ async function runInteractiveLoginFlow(options) {
76588
+ await ensureProfileDir(options.userDataDir);
76589
+ const activeBrowser = await launchSearchBrowser({
76590
+ headed: true,
76591
+ lang: options.lang,
76592
+ userDataDir: options.userDataDir
76593
+ });
76594
+ printLoginInstructions(options.userDataDir);
76595
+ try {
76596
+ await runLoginSession({
76597
+ browser: activeBrowser.browser,
76598
+ gl: options.gl,
76599
+ lang: options.lang
76600
+ });
76601
+ } finally {
76602
+ await closeSearchBrowser(activeBrowser);
76603
+ }
76604
+ await markProfilePrepared(options.userDataDir);
76605
+ }
76606
+ async function ensureInteractiveLogin(options) {
76607
+ await ensureProfileDir(options.userDataDir);
76608
+ if (await hasPreparedProfile(options.userDataDir)) {
76609
+ return false;
76610
+ }
76611
+ console.log("No prepared Google profile was found for this user-data-dir.");
76612
+ console.log("A login step is required before the first search.");
76613
+ await runInteractiveLoginFlow(options);
76614
+ return true;
76615
+ }
76616
+
76617
+ // src/cli/schema.ts
76618
+ import path13 from "path";
76562
76619
 
76563
76620
  // node_modules/zod/v4/classic/external.js
76564
76621
  var exports_external = {};
@@ -77325,10 +77382,10 @@ function mergeDefs(...defs) {
77325
77382
  function cloneDef(schema) {
77326
77383
  return mergeDefs(schema._zod.def);
77327
77384
  }
77328
- function getElementAtPath(obj, path12) {
77329
- if (!path12)
77385
+ function getElementAtPath(obj, path13) {
77386
+ if (!path13)
77330
77387
  return obj;
77331
- return path12.reduce((acc, key) => acc?.[key], obj);
77388
+ return path13.reduce((acc, key) => acc?.[key], obj);
77332
77389
  }
77333
77390
  function promiseAllObject(promisesObj) {
77334
77391
  const keys = Object.keys(promisesObj);
@@ -77709,11 +77766,11 @@ function aborted(x, startIndex = 0) {
77709
77766
  }
77710
77767
  return false;
77711
77768
  }
77712
- function prefixIssues(path12, issues) {
77769
+ function prefixIssues(path13, issues) {
77713
77770
  return issues.map((iss) => {
77714
77771
  var _a4;
77715
77772
  (_a4 = iss).path ?? (_a4.path = []);
77716
- iss.path.unshift(path12);
77773
+ iss.path.unshift(path13);
77717
77774
  return iss;
77718
77775
  });
77719
77776
  }
@@ -77896,7 +77953,7 @@ function formatError(error, mapper = (issue2) => issue2.message) {
77896
77953
  }
77897
77954
  function treeifyError(error, mapper = (issue2) => issue2.message) {
77898
77955
  const result = { errors: [] };
77899
- const processError = (error2, path12 = []) => {
77956
+ const processError = (error2, path13 = []) => {
77900
77957
  var _a4, _b2;
77901
77958
  for (const issue2 of error2.issues) {
77902
77959
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -77906,7 +77963,7 @@ function treeifyError(error, mapper = (issue2) => issue2.message) {
77906
77963
  } else if (issue2.code === "invalid_element") {
77907
77964
  processError({ issues: issue2.issues }, issue2.path);
77908
77965
  } else {
77909
- const fullpath = [...path12, ...issue2.path];
77966
+ const fullpath = [...path13, ...issue2.path];
77910
77967
  if (fullpath.length === 0) {
77911
77968
  result.errors.push(mapper(issue2));
77912
77969
  continue;
@@ -77938,8 +77995,8 @@ function treeifyError(error, mapper = (issue2) => issue2.message) {
77938
77995
  }
77939
77996
  function toDotPath(_path) {
77940
77997
  const segs = [];
77941
- const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
77942
- for (const seg of path12) {
77998
+ const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
77999
+ for (const seg of path13) {
77943
78000
  if (typeof seg === "number")
77944
78001
  segs.push(`[${seg}]`);
77945
78002
  else if (typeof seg === "symbol")
@@ -89686,13 +89743,13 @@ function resolveRef(ref, ctx) {
89686
89743
  if (!ref.startsWith("#")) {
89687
89744
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
89688
89745
  }
89689
- const path12 = ref.slice(1).split("/").filter(Boolean);
89690
- if (path12.length === 0) {
89746
+ const path13 = ref.slice(1).split("/").filter(Boolean);
89747
+ if (path13.length === 0) {
89691
89748
  return ctx.rootSchema;
89692
89749
  }
89693
89750
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
89694
- if (path12[0] === defsKey) {
89695
- const key = path12[1];
89751
+ if (path13[0] === defsKey) {
89752
+ const key = path13[1];
89696
89753
  if (!key || !ctx.defs[key]) {
89697
89754
  throw new Error(`Reference not found: ${ref}`);
89698
89755
  }
@@ -90101,7 +90158,7 @@ var sharedSchema = exports_external.object({
90101
90158
  headed: exports_external.boolean().default(false),
90102
90159
  json: exports_external.boolean().default(false),
90103
90160
  lang: defaultedString("lang", "en"),
90104
- userDataDir: defaultedString("userDataDir", defaultUserDataDir()).transform((value) => path12.resolve(value))
90161
+ userDataDir: defaultedString("userDataDir", defaultUserDataDir()).transform((value) => path13.resolve(value))
90105
90162
  });
90106
90163
  var searchSchema = sharedSchema.extend({
90107
90164
  num: positiveInteger("num").default(3),
@@ -90145,25 +90202,6 @@ function normalizeLoginOptions(input2) {
90145
90202
  }
90146
90203
 
90147
90204
  // 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
90205
  function createLoginArgs() {
90168
90206
  return {
90169
90207
  gl: {
@@ -90190,7 +90228,7 @@ async function runLogin(args) {
90190
90228
  lang: typeof args.lang === "string" ? args.lang : undefined,
90191
90229
  userDataDir: typeof args.userDataDir === "string" ? args.userDataDir : undefined
90192
90230
  });
90193
- await runLoginFlow(options.gl, options.lang, options.userDataDir);
90231
+ await runInteractiveLoginFlow(options);
90194
90232
  }
90195
90233
  function createLoginCommand(commandName = "findweb login") {
90196
90234
  return defineCommand({
@@ -90207,13 +90245,12 @@ function createLoginCommand(commandName = "findweb login") {
90207
90245
  }
90208
90246
 
90209
90247
  // src/cli/commands/search.ts
90210
- import fs9 from "fs/promises";
90211
90248
  import process4 from "process";
90212
90249
 
90213
90250
  // src/search/blocker.ts
90214
90251
  import fs8 from "fs/promises";
90215
90252
  import os9 from "os";
90216
- import path13 from "path";
90253
+ import path14 from "path";
90217
90254
 
90218
90255
  // node_modules/tldts-core/dist/es6/src/domain.js
90219
90256
  function shareSameDomainSuffix(hostname3, vhost) {
@@ -99477,8 +99514,8 @@ class FilterEngine extends EventEmitter5 {
99477
99514
  if (caching === undefined) {
99478
99515
  return init();
99479
99516
  }
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)));
99517
+ const { path: path14, read, write } = caching;
99518
+ return read(path14).then((buffer) => this.deserialize(buffer)).catch(() => init().then((engine) => write(path14, engine.serialize()).then(() => engine)));
99482
99519
  }
99483
99520
  static empty(config3 = {}) {
99484
99521
  return new this({ config: config3 });
@@ -101270,13 +101307,13 @@ class PuppeteerBlocker extends FilterEngine {
101270
101307
  // src/search/blocker.ts
101271
101308
  var blockerPromise;
101272
101309
  function defaultCacheDir() {
101273
- return process.env.GOOGLE_SEARCH_CACHE_DIR ?? path13.join(os9.homedir(), ".cache", "google-search");
101310
+ return process.env.GOOGLE_SEARCH_CACHE_DIR ?? path14.join(os9.homedir(), ".cache", "google-search");
101274
101311
  }
101275
101312
  async function readCache(filePath) {
101276
101313
  return fs8.readFile(filePath);
101277
101314
  }
101278
101315
  async function writeCache(filePath, buffer) {
101279
- await fs8.mkdir(path13.dirname(filePath), { recursive: true });
101316
+ await fs8.mkdir(path14.dirname(filePath), { recursive: true });
101280
101317
  await fs8.writeFile(filePath, buffer);
101281
101318
  }
101282
101319
  async function loadBlocker() {
@@ -101285,7 +101322,7 @@ async function loadBlocker() {
101285
101322
  const cacheDir = defaultCacheDir();
101286
101323
  await fs8.mkdir(cacheDir, { recursive: true });
101287
101324
  return PuppeteerBlocker.fromPrebuiltAdsAndTracking(globalThis.fetch.bind(globalThis), {
101288
- path: path13.join(cacheDir, "ghostery-engine.bin"),
101325
+ path: path14.join(cacheDir, "ghostery-engine.bin"),
101289
101326
  read: readCache,
101290
101327
  write: writeCache
101291
101328
  });
@@ -101339,9 +101376,6 @@ function exitCodeForOutcomes(items) {
101339
101376
  }
101340
101377
 
101341
101378
  // src/cli/commands/search.ts
101342
- async function ensureProfileDir2(dirPath) {
101343
- await fs9.mkdir(dirPath, { recursive: true });
101344
- }
101345
101379
  function printResults(json2, outcomes) {
101346
101380
  if (json2) {
101347
101381
  printJsonResults(outcomes);
@@ -101402,7 +101436,16 @@ async function runSearch(args) {
101402
101436
  parallel: typeof args.parallel === "string" || typeof args.parallel === "number" ? args.parallel : undefined,
101403
101437
  userDataDir: typeof args.userDataDir === "string" ? args.userDataDir : undefined
101404
101438
  });
101405
- await ensureProfileDir2(options.userDataDir);
101439
+ await ensureProfileDir(options.userDataDir);
101440
+ const loginWasRequired = await ensureInteractiveLogin({
101441
+ gl: options.gl,
101442
+ lang: options.lang,
101443
+ userDataDir: options.userDataDir
101444
+ });
101445
+ if (loginWasRequired) {
101446
+ console.log(`Login completed. Continuing with search...
101447
+ `);
101448
+ }
101406
101449
  const blocker = await loadBlocker();
101407
101450
  const activeBrowser = await launchSearchBrowser({
101408
101451
  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.1",
4
4
  "description": "Google search CLI powered by system Chrome",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.11",