browsemind 0.5.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/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 browsemind Contributors
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published
8
+ by the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ Full license text: https://www.gnu.org/licenses/agpl-3.0.txt
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,896 @@
1
+ # BrowseMind 🤖 — LLM Browser Automation Agent
2
+
3
+ > **Navigate, search, extract, and automate any website using natural language.**
4
+ > Powered by Crawl4AI and LiteLLM — the agent decides everything, you just describe what you need.
5
+
6
+ <div align="center">
7
+
8
+ [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
9
+ [![Version](https://img.shields.io/badge/version-0.5.0-green.svg)](https://github.com/prokopis3/browsemind/releases)
10
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPLv3-blue.svg)](LICENSE)
11
+ [![Crawl4AI](https://img.shields.io/badge/Crawl4AI-0.9.0%2B-orange)](https://github.com/unclecode/crawl4ai)
12
+ [![SemVer](https://img.shields.io/badge/SemVer-0.5.0-ff69b4.svg)](https://semver.org/spec/v2.0.0.html)
13
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://www.conventionalcommits.org/)
14
+ [![Changelog](https://img.shields.io/badge/Changelog-Keep%20a%20Changelog-brightgreen.svg)](https://keepachangelog.com/en/1.1.0/)
15
+ [![CI](https://github.com/prokopis3/browsemind/workflows/CI/badge.svg)](https://github.com/prokopis3/browsemind/actions)
16
+ **[Installation](#installation)** • **[Quickstart](#rocket-quickstart)** • **[CLI Reference](#computer-cli-reference)** • **[Features](#sparkles-features)** • **[Architecture](#architecture)** • **[Configuration](#wrench-configuration)** • **[API](run_api.py)** • **[Roadmap](ROADMAP.md)**
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## 📋 Table of Contents
23
+
24
+ - [Installation](#installation)
25
+ - [Quickstart](#rocket-quickstart)
26
+ - [CLI Reference](#computer-cli-reference)
27
+ - [Features](#sparkles-features)
28
+ - [Architecture](#architecture)
29
+ - [Python API](#snake-python-api)
30
+ - [Configuration](#wrench-configuration)
31
+ - [Testing](#microscope-testing)
32
+ - [Project Structure](#open_file_folder-project-structure)
33
+ - [Contributing](#handshake-contributing)
34
+ - [License](#page_with_curl-license)
35
+ - [Disclaimer](#disclaimer)
36
+ - [Citations](#-citations-1)
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ### Requirements
43
+
44
+ - **Python 3.11+**
45
+ - **Chrome/Chromium** — installed automatically by Crawl4AI on first run, or manually via [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/)
46
+ - **Playwright** — installed via `playwright install chromium` (auto-setup with Crawl4AI)
47
+
48
+ <details>
49
+ <summary><strong>Package Managers</strong></summary>
50
+
51
+ ### pip (recommended)
52
+
53
+ ```bash
54
+ pip install browsemind
55
+ browsemind doctor # Verify installation
56
+ browsemind --task "Open example.com and screenshot it"
57
+ ```
58
+
59
+ ### uv
60
+
61
+ ```bash
62
+ uv tool install browsemind
63
+ browsemind doctor
64
+ browsemind --task "Open example.com and screenshot it"
65
+ ```
66
+
67
+ ### npm
68
+
69
+ ```bash
70
+ npm install -g browsemind
71
+ browsemind doctor
72
+ browsemind --task "Open example.com and screenshot it"
73
+ ```
74
+
75
+ ### Homebrew (macOS)
76
+
77
+ ```bash
78
+ brew install browsemind/browsemind/browsemind
79
+ browsemind doctor
80
+ ```
81
+
82
+ ### Chocolatey (Windows)
83
+
84
+ ```powershell
85
+ choco install browsemind
86
+ browsemind doctor
87
+ ```
88
+
89
+ ### Scoop (Windows)
90
+
91
+ ```powershell
92
+ scoop bucket add browsemind https://github.com/prokopis3/scoop-browsemind
93
+ scoop install browsemind
94
+ browsemind doctor
95
+ ```
96
+
97
+ </details>
98
+
99
+ <details>
100
+ <summary><strong>Development & Quick Start</strong></summary>
101
+
102
+ ### From Source (development)
103
+
104
+ ```bash
105
+ git clone https://github.com/prokopis3/browsemind.git
106
+ cd browsemind
107
+
108
+ # Create virtual environment
109
+ python -m venv .venv
110
+ source .venv/bin/activate # Linux/macOS
111
+ .venv\Scripts\activate # Windows
112
+
113
+ # Install
114
+ uv pip install -e .
115
+
116
+ # Install Playwright browsers
117
+ playwright install chromium
118
+
119
+ # Verify
120
+ browsemind doctor
121
+ ```
122
+
123
+ ### Quick Start (no install)
124
+
125
+ ```bash
126
+ npx browsemind doctor
127
+ npx browsemind --task "Open example.com and screenshot it"
128
+ ```
129
+
130
+ </details>
131
+
132
+ <details>
133
+ <summary><strong>Updating & Diagnostics</strong></summary>
134
+
135
+ ### Updating
136
+
137
+ Upgrade to the latest version:
138
+
139
+ ```bash
140
+ browsemind upgrade
141
+ ```
142
+
143
+ Auto-detects your installation method (pip, uv, npm, Homebrew, Chocolatey, Scoop) and runs the appropriate update command. Displays the version change on success.
144
+
145
+ To check for updates without upgrading:
146
+
147
+ ```bash
148
+ browsemind upgrade --check
149
+ ```
150
+
151
+ ### Doctor — Diagnose Your Installation
152
+
153
+ Run `doctor` whenever something stops working unexpectedly or after upgrades:
154
+
155
+ ```bash
156
+ browsemind doctor # Full diagnosis
157
+ browsemind doctor --offline --quick # Local-only, fastest
158
+ browsemind doctor --fix # Also run destructive repairs
159
+ browsemind doctor --json # Structured output for agents
160
+ ```
161
+
162
+ Checks: environment, Chrome install, config files, security, network reachability, and a live headless launch test. Exit code `0` if all pass, `1` if any fail.
163
+
164
+ </details>
165
+
166
+ ---
167
+
168
+ ## 🚀 Quickstart
169
+
170
+ ### 1. Set Your API Key
171
+
172
+ ```bash
173
+ # Copy the example env file
174
+ cp llm.env.example llm.env
175
+
176
+ # Edit llm.env with at least one API key:
177
+ # DEEPSEEK_API_KEY=sk-your-deepseek-key
178
+ # OPENAI_API_KEY=sk-your-openai-key
179
+ # GROQ_API_KEY=gsk-your-groq-key
180
+ ```
181
+
182
+ ### 2. Run Your First Task
183
+
184
+ ```bash
185
+ # Browse and screenshot
186
+ browsemind --task "Navigate to example.com and take a screenshot"
187
+
188
+ # Extract data
189
+ browsemind --task "Go to quotes.toscrape.com and extract all quotes" --headless
190
+
191
+ # Search + extract
192
+ browsemind --task "Search for Python async programming and return the top 3 results"
193
+
194
+ # Deep crawl a catalog
195
+ browsemind --task "Extract all product titles and prices" --url https://books.toscrape.com --discover
196
+
197
+ # Fast content extraction (Queen Reader — no LLM, no browser wait)
198
+ browsemind --task "Read the text from example.com"
199
+ ```
200
+
201
+ ### 3. Try Command Mode
202
+
203
+ ```bash
204
+ browsemind open https://example.com
205
+ browsemind snapshot
206
+ browsemind screenshot
207
+ browsemind close
208
+ ```
209
+
210
+ ---
211
+
212
+ ## 💻 CLI Reference
213
+
214
+ <details>
215
+ <summary><strong>Task Mode (AI Agent)</strong></summary>
216
+
217
+ ```bash
218
+ browsemind --task "Extract all product names and prices" --url https://books.toscrape.com
219
+ browsemind --task "Find the pricing page and return the prices" --url https://example.com
220
+ browsemind --task "Extract all articles from the blog" --save json --output-dir ./output
221
+ browsemind --task "Access the dashboard and download the report" --url https://example.com/login --login
222
+ browsemind --task "Capture the full page" --url https://example.com --vision-tile
223
+ ```
224
+
225
+ **Options:**
226
+
227
+ | Flag | Description |
228
+ |------|-------------|
229
+ | `--task`, `-t` | Natural language task description |
230
+ | `--url` | Starting URL |
231
+ | `--headless` | Run browser in headless mode |
232
+ | `--max-steps` | Maximum agent steps (default: 30) |
233
+ | `--save json/csv/both` | Save extracted data to file |
234
+ | `--screenshot` | Save screenshot after extraction |
235
+ | `--pdf` | Generate PDF of the page |
236
+ | `--markdown` | Generate markdown summary |
237
+ | `--discover` | Deep crawl domain discovery |
238
+ | `--discover-max-pages` | Max pages for discovery (default: 50) |
239
+ | `--login` | Force interactive login mode |
240
+ | `--json` | JSON output format |
241
+ | `--verbose`, `-v` | Verbose output |
242
+
243
+ </details>
244
+
245
+ <details>
246
+ <summary><strong>Command Mode — Core & Interactions</strong></summary>
247
+
248
+ #### Core
249
+
250
+ ```bash
251
+ browsemind open <url> # Navigate to URL
252
+ browsemind fetch <url> # Fetch webpage content
253
+ browsemind snapshot # Get page state (element refs, text, URL)
254
+ browsemind screenshot [path] # Take a screenshot
255
+ browsemind close # Close browser
256
+ ```
257
+
258
+ #### Interactions — use element refs from `snapshot`
259
+
260
+ ```bash
261
+ browsemind click @e3 # Click element by ref
262
+ browsemind fill @e2 "hello world" # Clear and fill input
263
+ browsemind type @e2 "text" # Type into element
264
+ browsemind press Enter # Press keyboard key
265
+ browsemind scroll down # Scroll page
266
+ browsemind highlight .product-card # Highlight element
267
+ ```
268
+
269
+ </details>
270
+
271
+ <details>
272
+ <summary><strong>Command Mode — Page Info, Tabs & Debug</strong></summary>
273
+
274
+ #### Page Information
275
+
276
+ ```bash
277
+ browsemind get title # Page title
278
+ browsemind get url # Current URL
279
+ browsemind get text h1 # Element text
280
+ browsemind get html .content # Element HTML
281
+ browsemind get count .product # Element count
282
+ browsemind is visible .loading # Check visibility
283
+ browsemind cookies list # List cookies
284
+ browsemind storage get key # localStorage value
285
+ ```
286
+
287
+ #### Tabs & Navigation
288
+
289
+ ```bash
290
+ browsemind tab list # List open tabs
291
+ browsemind tab new <url> # Open new tab
292
+ browsemind tab switch 2 # Switch to tab
293
+ browsemind back # Go back
294
+ browsemind forward # Go forward
295
+ browsemind reload # Reload page
296
+ ```
297
+
298
+ #### CDP & Debug
299
+
300
+ ```bash
301
+ browsemind cdp # Get CDP WebSocket URL
302
+ browsemind console list # View JS console messages
303
+ browsemind errors list # View page errors
304
+ browsemind inspect # Open Chrome DevTools
305
+ browsemind pdf page.pdf # Save page as PDF
306
+ browsemind trace start # Start performance tracing
307
+ browsemind wait 5 # Wait N seconds
308
+ ```
309
+
310
+ </details>
311
+
312
+ <details>
313
+ <summary><strong>Command Mode — Maintenance, Plugins & Fetch</strong></summary>
314
+
315
+ #### Setup & Maintenance
316
+
317
+ ```bash
318
+ browsemind upgrade # Upgrade to latest version
319
+ browsemind upgrade --check # Check version without upgrading
320
+ browsemind doctor # Diagnose installation
321
+ browsemind doctor --fix # Diagnose + repair
322
+ browsemind doctor --json # Structured JSON output
323
+ ```
324
+
325
+ #### Skills, Plugins & MCP
326
+
327
+ ```bash
328
+ browsemind skills list # List installed skills
329
+ browsemind skills add <source> # Install skill from GitHub/URL
330
+ browsemind skills get <name> # Show skill details
331
+ browsemind skills remove <name> # Uninstall skill
332
+
333
+ browsemind plugin list # List installed plugins
334
+ browsemind plugin add <name> # Install plugin
335
+ browsemind plugin run <name> # Execute plugin
336
+
337
+ browsemind mcp --tools all # Start MCP server
338
+ ```
339
+
340
+ #### Fetch Options
341
+
342
+ ```bash
343
+ browsemind fetch <url> --format markdown # Default
344
+ browsemind fetch <url> --format json # Structured JSON
345
+ browsemind fetch <url> --format screenshot # Screenshot
346
+ browsemind fetch --search "query" # Search then fetch
347
+ ```
348
+
349
+ </details>
350
+
351
+ ---
352
+
353
+ ## ✨ Features
354
+
355
+ <details>
356
+ <summary><strong>🧠 LLM Decision Engine</strong></summary>
357
+
358
+ `AgentBrain` uses LiteLLM to observe, think, decide, and verify — accuracy over cost. The system prompt is a compact **97-line / ~959-token decision tree** (was 580 lines / ~10K tokens — 6x smaller). The LLM chooses from **141 ActionType enum values** covering every pattern in the [Dynamic Data Extraction Guide](docs/DYNAMIC_DATA_EXTRACTION_GUIDE.md).
359
+
360
+ </details>
361
+
362
+ <details>
363
+ <summary><strong>🌐 Intelligent Browser Automation</strong></summary>
364
+
365
+ Full Playwright integration via Crawl4AI with anti-bot evasion, stealth mode, managed browser profiles, and persistent sessions. The **PageNavigator** provides a 6-strategy escalation chain:
366
+
367
+ ```
368
+ DomainGraph cache → smart goto → navigate_js → proxy escalation → undetected browser → fallback fetch
369
+ ```
370
+
371
+ Cloudflare, Akamai, CAPTCHA, and WAF detection built in.
372
+
373
+ </details>
374
+
375
+ <details>
376
+ <summary><strong>📊 Multi-Cascade Extraction (18 Pipelines)</strong></summary>
377
+
378
+ Cost-ascending extraction cascade — LLM is the **LAST** resort:
379
+
380
+ ```
381
+ Identity → QueenReader → Markdown → CSS → XPath → Regex → Cosine → LLM → Vision
382
+ ```
383
+
384
+ | Pipeline | Cost | Method |
385
+ |----------|------|--------|
386
+ | **Identity** (pass-through) | $0 | `NoExtractionStrategy` — raw content |
387
+ | **Queen Reader** | $0 | DOMContentLoaded fast path — no schema, no LLM, no vision |
388
+ | **CSS Extraction** | $0 | `JsonCssExtractionStrategy` — structured lists |
389
+ | **XPath Extraction** | $0 | `JsonXPathExtractionStrategy` — complex DOM |
390
+ | **Regex Extraction** | $0 | `RegexExtractionStrategy` — 23 built-in patterns |
391
+ | **Cosine Extraction** | $0 | `CosineStrategy` — semantic clustering |
392
+ | **LLM Extraction** | $$ | `LLMExtractionStrategy` — unstructured content |
393
+ | **Vision Extraction** | $$$ | Screenshot analysis via LLM |
394
+ | **Deep Crawl** | Varies | `AdaptiveCrawler` — full site crawl |
395
+ | **Batch** | Varies | `arun_many()` — parallel multi-URL |
396
+ | **Scroll / Load-More** | $0 | Infinite scroll / AJAX load-more |
397
+ | **Table / PDF / Video** | $0 | Specialized pipelines |
398
+ | **Chunked LLM** | $$ | 3 chunking strategies (regex, sliding, overlapping) |
399
+
400
+ </details>
401
+
402
+ <details>
403
+ <summary><strong>📖 Queen Reader — Content-First Fast Extraction</strong></summary>
404
+
405
+ Inspired by PixelRAG's approach. Navigates with `wait_until='domcontentloaded'` (not `networkidle`) — returns text content **before** the full page loads. No schema generation, no LLM cascade, no waiting for JS/images. Falls back to full load if content < 500 chars.
406
+
407
+ </details>
408
+
409
+ <details>
410
+ <summary><strong>🔢 DOM Serializer & Element Highlighter</strong></summary>
411
+
412
+ browser-use style indexed interactive element mapping. Runs JS to find ALL interactive DOM elements, assigns stable **1-based indices**, builds `selector_map {index → backendNodeId}` for CDP dispatch. The Element Highlighter draws numbered blue outline boxes on each element so the LLM can reference `click(index=N)` or `input(index=N, text="...")`.
413
+
414
+ </details>
415
+
416
+ <details>
417
+ <summary><strong>🔐 Login & Credential Management</strong></summary>
418
+
419
+ Persisted browser profiles, encrypted credential vault, a11y snapshot form filling (works on React/Vue/Angular), multi-strategy login pipeline with wrong-credential retry. Dedicated `LOGIN` + `RECOVER_FROM_BLOCK` action handlers.
420
+
421
+ </details>
422
+
423
+ <details>
424
+ <summary><strong>⚡ Token Budget-Aware LLM Tiering</strong></summary>
425
+
426
+ BrowseMind automatically selects the optimal LLM tier based on your remaining **prompt token budget**:
427
+
428
+ | Tier | Budget | Behavior | Model |
429
+ |------|--------|----------|-------|
430
+ | **🟢 RULE** | &lt; 500 tokens | Zero-LLM — rule-based fallback only | No LLM call |
431
+ | **🟡 CHEAP** | 500–2,000 tokens | Low-cost LLM for simple decisions | `deepseek/deepseek-chat` (or configured cheap provider) |
432
+ | **🔴 FULL** | &gt; 2,000 tokens | Full LLM for complex reasoning | Primary model (DeepSeek/GPT-4/Claude) |
433
+
434
+ Thresholds are configurable in `browsemind.yml`:
435
+
436
+ ```yaml
437
+ llm:
438
+ cheap_threshold: 500 # Below this → RULE tier
439
+ full_threshold: 2000 # Above this → FULL tier
440
+ cheap_provider: "deepseek/deepseek-chat" # $0.14/$0.28 per 1M tokens
441
+ ```
442
+
443
+ **The stack:**
444
+ - **TokenTracker** — Tracks prompt/completion tokens and total cost per session
445
+ - **LLMTierSelector** — Picks RULE/CHEAP/FULL based on remaining budget
446
+ - **TokenOptimizer** — 14-pattern RuleActionSuggester, FTS5LinkMatcher, BM25Ranker (~80% prompt reduction)
447
+ - **ThresholdSupervisor** — Prevents budget overshoot
448
+ - **Chain failure recovery** — Auto-cascade to next strategy on failure
449
+
450
+ </details>
451
+
452
+ <details>
453
+ <summary><strong>🔌 Plugin System</strong></summary>
454
+
455
+ Three plugin types extend the agent:
456
+
457
+ - **ToolPlugin** — Add new tools to ToolRegistry (Slack, email, etc.)
458
+ - **SkillPlugin** — Prompt-level skills (constraints, context injection)
459
+ - **MCPPlugin** — Wrap MCP servers as agent tools (filesystem, GitHub)
460
+
461
+ </details>
462
+
463
+ <details>
464
+ <summary><strong>🧰 38 Built-in Tool Wrappers</strong></summary>
465
+
466
+ Discoverable tools via ToolRegistry with structured metadata, parameter validation, and failure/success pattern documentation for LLM use. Includes QueenReaderTool, DOMSerializerTool, ElementHighlighterTool.
467
+
468
+ </details>
469
+
470
+ <details>
471
+ <summary><strong>📦 28 Handler Files, 48 Handlers, 141 ActionTypes</strong></summary>
472
+
473
+ Single `ActionDispatcher` routes all 141 action types through 48 specialized handler classes — no if/elif chains.
474
+
475
+ </details>
476
+
477
+ <details>
478
+ <summary><strong>🔎 4 Search Engine Support</strong></summary>
479
+
480
+ Google (CSE API + browser fallback, sign-in redirect fixed), Bing, Brave, DuckDuckGo — auto-fallback orchestration.
481
+
482
+ </details>
483
+
484
+ <details>
485
+ <summary><strong>🧪 JS Browser-Level Test Suite</strong></summary>
486
+
487
+ 19 Playwright-based tests in `tests_js/` covering DOM serializer, element highlighter, form interaction, consent manager, human-like behavior, recovery engine, strategy pipeline, tile capture.
488
+
489
+ </details>
490
+
491
+ ---
492
+
493
+ ## Architecture
494
+
495
+ ### Core Loop
496
+
497
+ ```
498
+ User Prompt → Entry (CLI/API)
499
+ → AgentLoop.run()
500
+ → PageState.capture() # OBSERVE
501
+ → AgentBrain.assess_progress() # THINK
502
+ → AgentBrain.decide() # DECIDE (LLM)
503
+ → ActionDispatcher.dispatch() # ACT
504
+ → Specific Handler → BrowserAgent
505
+ → AgentBrain.verify() # VERIFY
506
+ → Loop until DONE
507
+ ```
508
+
509
+ ### Architecture Diagram (Mermaid)
510
+
511
+ See [`docs/architecture.mmd`](docs/architecture.mmd) for the full interactive diagram. Key layers:
512
+
513
+ | Layer | Description | Key Files |
514
+ |-------|-------------|-----------|
515
+ | **1. Entry** | CLI + FastAPI | `main.py`, `run_api.py`, `lib/cli/` |
516
+ | **2. Agent Loop & Brain** | Observe→Think→Decide→Act→Verify | `agent_loop.py`, `agent_brain.py`, `page_state.py` |
517
+ | **3. Tool Registry** | 38 AgentTool wrappers + Plugin System | `tools/tool_registry.py`, `tools/builtin_tools.py` |
518
+ | **4. Action Dispatch** | Single dispatch authority | `handlers/__init__.py` |
519
+ | **5. Handler Layer** | 48 handlers across 28 files | `handlers/interaction.py`, `handlers/extraction.py`, ... |
520
+ | **6. Browser Layer** | Unified API (50+ methods) | `browser_agent.py`, `navigator.py` |
521
+ | **7. Extraction Layer** | 18 pipelines, cost-ascending cascade | `strategy_pipeline.py`, `queen_reader.py` |
522
+ | **8. Content Pipeline** | Queen Reader, DOM Serializer, Highlighter | `queen_reader.py`, `dom_serializer.py`, `element_highlighter.py` |
523
+ | **9. Support & Infrastructure** | Cache, Memory, Proxy, Identity, Telemetry | `cache.py`, `memory/`, `proxy_manager.py`, `identity_manager.py` |
524
+ | **10. Multi-Agent System** | Planner/Verifier/Supervisor (advisory) | `agents/planner.py`, `agents/verifier.py` |
525
+
526
+ ### Key Metrics
527
+
528
+ | Metric | Value |
529
+ |--------|-------|
530
+ | Action types | **141** |
531
+ | Handler files | **28** |
532
+ | Concrete handlers | **48** |
533
+ | AgentTool wrappers | **38** |
534
+ | Extraction pipelines | **18** |
535
+ | Search engines | **4** |
536
+ | Cache tiers | **4** (L0–L3) |
537
+ | Memory tiers | **3** (Working/Episodic/Semantic) |
538
+ | Plugin types | **3** (Tool/Skill/MCP) |
539
+ | Largest file | `strategy_pipeline.py` (~4,400 lines) |
540
+ | System prompt | **97 lines / ~959 tokens** |
541
+ | JS test suite | **19 files** in `tests_js/` |
542
+
543
+ ### Key Design Principles
544
+
545
+ | Principle | Description |
546
+ |-----------|-------------|
547
+ | **LLM is Primary** | Accuracy over cost — LLM consulted first for every decision |
548
+ | **Rules Suggest** | RuleActionSuggester provides hints, not commands |
549
+ | **Zero-LLM by Default** | Extraction, navigation, link matching use 0 LLM tokens |
550
+ | **Cost-Ascending Cascade** | Identity→CSS→...→LLM→Vision (LLM is absolute LAST) |
551
+ | **No Hardcoded Names** | Element recognition uses HTML semantics + WAI-ARIA |
552
+ | **Structural Overlays** | Consent dialogs detected by `role=dialog`, `position:fixed` |
553
+
554
+ ---
555
+
556
+ ## 🐍 Python API
557
+
558
+ ```python
559
+ import asyncio
560
+ from lib.agent_loop import AgentLoop
561
+ from lib.config import AppConfig
562
+
563
+ async def main():
564
+ config = AppConfig.load("browsemind.yml")
565
+ loop = AgentLoop(config)
566
+ result = await loop.run("Navigate to example.com and extract all headings")
567
+ print(f"Success: {result.success}")
568
+ print(f"Steps: {result.steps_taken}")
569
+ print(f"Data: {result.data}")
570
+ print(f"Cost: ${result.total_cost:.4f}")
571
+
572
+ asyncio.run(main())
573
+ ```
574
+
575
+ ### Programmatic Extraction
576
+
577
+ ```python
578
+ from lib.strategy_pipeline import StrategyPipeline
579
+ from lib.crawl4ai_config import Crawl4AIConfigBuilder
580
+
581
+ config = (
582
+ Crawl4AIConfigBuilder()
583
+ .with_css_selector(".product")
584
+ .with_word_count_threshold(50)
585
+ .with_cache_mode("BYPASS")
586
+ .build()
587
+ )
588
+
589
+ pipeline = StrategyPipeline(crawler)
590
+ result = await pipeline.run(
591
+ url="https://books.toscrape.com",
592
+ strategy="css",
593
+ schema={
594
+ "baseSelector": ".product",
595
+ "fields": [
596
+ {"name": "title", "selector": "h3 a", "type": "text"},
597
+ {"name": "price", "selector": ".price", "type": "text"},
598
+ ]
599
+ }
600
+ )
601
+ print(result.extracted[:5])
602
+ ```
603
+
604
+ ---
605
+
606
+ ## 🔧 Configuration
607
+
608
+ ### LLM Providers (YAML-Driven)
609
+
610
+ Configuration lives in a single file: [`browsemind.yml`](browsemind.yml). API keys go in `llm.env` (git-ignored).
611
+
612
+ ```yaml
613
+ llm:
614
+ default_provider: "deepseek/deepseek-chat"
615
+ extraction_provider: "deepseek/deepseek-chat"
616
+ config:
617
+ temperature: 0.3
618
+ max_tokens: 4096
619
+ ```
620
+
621
+ **llm.env:**
622
+
623
+ ```env
624
+ DEEPSEEK_API_KEY=sk-your-deepseek-key
625
+ # OPENAI_API_KEY=sk-your-openai-key
626
+ # GROQ_API_KEY=gsk-your-groq-key
627
+ # ANTHROPIC_API_KEY=sk-ant-your-anthropic-key
628
+ ```
629
+
630
+ Supported providers (any LiteLLM-compatible):
631
+
632
+ - `deepseek/deepseek-chat` — **default** ($0.14/$0.28 per 1M tokens, cheapest June 2026)
633
+ - `deepseek/deepseek-v4-pro` — Premium tier ($0.435/$0.87 per 1M tokens)
634
+ - `openai/gpt-4o-mini`, `openai/gpt-4o`
635
+ - `groq/meta-llama/llama-4-scout-17b-16e-instruct`
636
+ - `anthropic/claude-3-haiku`, `anthropic/claude-3-opus`
637
+ - Any OpenAI-compatible endpoint
638
+
639
+ ### Extraction Configuration
640
+
641
+ 18 LLM-configurable parameters via `ExtractionConfig`:
642
+
643
+ | Parameter | Type | Description |
644
+ |-----------|------|-------------|
645
+ | `wait_for` | `str` | CSS/JS condition before extraction |
646
+ | `scan_full_page` | `bool` | Auto-scroll for infinite content |
647
+ | `word_count_threshold` | `int` | Min words per block (default: 200) |
648
+ | `css_selector` | `str` | Focus on specific page section |
649
+ | `excluded_tags` | `list` | Remove specific HTML tags |
650
+ | `remove_overlay_elements` | `bool` | Remove popups/modals |
651
+ | `simulate_user` | `bool` | Simulate mouse movements |
652
+ | `magic` | `bool` | Auto-handle popups/consent |
653
+ | `cache_mode` | `str` | `ENABLED/BYPASS/DISABLED/READ_ONLY/WRITE_ONLY` |
654
+ | `process_iframes` | `bool` | Inline iframe content |
655
+ | `flatten_shadow_dom` | `bool` | Flatten Shadow DOM |
656
+
657
+ ---
658
+
659
+ ## 🔬 Testing
660
+
661
+ ### Python Tests
662
+
663
+ ```bash
664
+ # Run all unit tests
665
+ pytest
666
+
667
+ # Run specific test file
668
+ pytest tests/test_improvements.py -v
669
+
670
+ # Run with coverage
671
+ pytest --cov=lib tests/
672
+ ```
673
+
674
+ ### JS Browser-Level Tests (19 Playwright tests)
675
+
676
+ ```bash
677
+ cd tests_js
678
+ node run_all.js
679
+ ```
680
+
681
+ Individual test files in `tests_js/`:
682
+
683
+ - `test_dom_serializer.js` — Element indexing and selector maps
684
+ - `test_element_highlighter.js` — Numbered overlay rendering
685
+ - `test_consent_manager.js` — Consent dismissal with overlay detection
686
+ - `test_form_interaction.js` — Form fill and submit flows
687
+ - `test_human_like_behavior.js` — Human-like behavior simulation
688
+ - `test_tile_capture.js` — Tile-based screenshot capture
689
+ - `test_strategy_pipeline.js` — Pipeline cascade behavior
690
+ - `test_recovery_engine.js` — Recovery strategy behavior
691
+
692
+ ### CDP & Navigation Diagnostics
693
+
694
+ ```bash
695
+ python tests/test_diagnose_cdp.py
696
+ python tests/test_diagnose_navigation.py
697
+ ```
698
+
699
+ ---
700
+
701
+ ## 📁 Project Structure
702
+
703
+ ```
704
+ browsemind/
705
+ ├── main.py # CLI entry (task mode + command mode)
706
+ ├── run_api.py # FastAPI server entry
707
+ ├── pyproject.toml # Project config + dependencies
708
+ ├── browsemind.yml # YAML-driven configuration
709
+ ├── llm.env / llm.env.example # API keys (git-ignored)
710
+
711
+ ├── lib/ # Core library
712
+ │ ├── agent_loop.py # Core iteration loop
713
+ │ ├── agent_brain.py # LLM decision engine (~1,771 lines)
714
+ │ ├── page_state.py # DOM observation (~620 lines)
715
+ │ ├── actions.py # 141 ActionTypes + dataclasses
716
+ │ ├── browser_agent.py # Unified browser API (~1,750 lines)
717
+ │ ├── strategy_pipeline.py # Single extraction authority (~4,400 lines)
718
+ │ ├── queen_reader.py # DOMContentLoaded fast path
719
+ │ ├── dom_serializer.py # browser-use style indexed elements
720
+ │ ├── element_highlighter.py # Numbered blue overlays
721
+ │ ├── navigator.py # Anti-bot strategy chain
722
+ │ ├── human_like_behavior.py # Mouse/keyboard emulation
723
+ │ ├── consent_manager.py # Overlay/modal removal
724
+ │ ├── login_pipeline.py # Automated login flows
725
+ │ │
726
+ │ ├── handlers/ # 28 files, 48 handlers, 141 action types
727
+ │ │ ├── interaction.py # Click, Input, Scroll, Hover, DblClick, Drag, Upload
728
+ │ │ ├── extraction.py # 11 extraction action types
729
+ │ │ ├── navigation.py # Navigate, session/proxy navigate
730
+ │ │ ├── download.py # Download, DownloadMany (yt-dlp)
731
+ │ │ ├── search_site.py # Intra-site page discovery
732
+ │ │ ├── tabs.py # Multi-tab + page history operations
733
+ │ │ └── ... # 22 more handler files
734
+ │ │
735
+ │ ├── tools/ # Tool Registry + SERP integrations
736
+ │ │ ├── tool_registry.py # Central registry
737
+ │ │ ├── builtin_tools.py # 38 AgentTool wrappers
738
+ │ │ ├── google_serp.py # Google search (CSE + browser)
739
+ │ │ ├── bing_serp.py # Bing search
740
+ │ │ ├── brave_serp.py # Brave search
741
+ │ │ └── duckduckgo_serp.py # DuckDuckGo search
742
+ │ │
743
+ │ ├── memory/ # 3-tier memory (SQLite-backed)
744
+ │ │ ├── working.py # Current session actions
745
+ │ │ ├── episodic.py # Past sessions
746
+ │ │ └── semantic.py # Cross-session patterns + FTS5
747
+ │ │
748
+ │ ├── agents/ # Multi-agent system (advisory)
749
+ │ │ ├── planner.py # Task decomposition
750
+ │ │ ├── verifier.py # Outcome verification
751
+ │ │ └── supervisor.py # Task supervision
752
+ │ │
753
+ │ ├── plugin/ # Plugin system
754
+ │ │ ├── base.py # Plugin ABCs
755
+ │ │ ├── plugin_manager.py # Lifecycle management
756
+ │ │ └── mcp_plugin.py # MCP server integration
757
+ │ │
758
+ │ ├── api/ # FastAPI REST
759
+ │ │ └── routes.py # 8 endpoints
760
+ │ │
761
+ │ └── cli/ # Unified CLI
762
+ │ ├── args.py # Argument parser
763
+ │ └── crwl_cli.py # Command implementations
764
+
765
+ ├── archive/plans/ # Archived design documents
766
+ ├── docs/ # Documentation
767
+ │ ├── architecture.mmd # Mermaid diagram
768
+ │ ├── DYNAMIC_DATA_EXTRACTION_GUIDE.md # 52 workflow states
769
+ │ └── ... # Crawl4AI docs cache
770
+
771
+ ├── tests/ # Python test suite
772
+ ├── tests_js/ # 19 Playwright-based browser tests
773
+ ├── tutorial/ # Crawl4AI tutorial scripts
774
+ └── output/ # Agent output directory
775
+ ```
776
+
777
+ ---
778
+
779
+ ## 🤝 Contributing
780
+
781
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
782
+
783
+ **Quick start:**
784
+
785
+ ```bash
786
+ git clone https://github.com/prokopis3/browsemind.git
787
+ cd browsemind
788
+ python -m venv .venv && source .venv/bin/activate
789
+ pip install -e ".[dev]"
790
+ playwright install chromium
791
+ ```
792
+
793
+ **Commit conventions:** We follow [Conventional Commits](https://www.conventionalcommits.org/) with SemVer. See [commitlint.config.js](commitlint.config.js) for allowed scopes and types.
794
+
795
+ **Code of Conduct:** [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
796
+
797
+ ---
798
+
799
+ ## 📚 Documentation
800
+
801
+ | Resource | Description |
802
+ |----------|-------------|
803
+ | [Architecture Deep Dive](ARCHITECTURE_DEEP_DIVE.md) | Comprehensive 16-section architecture analysis |
804
+ | [Roadmap](ROADMAP.md) | Future priorities, milestones, and research areas |
805
+ | [Changelog](CHANGELOG.md) | Full release history with categorized entries |
806
+ | [Architecture Diagram](docs/architecture.mmd) | Interactive Mermaid architecture diagram |
807
+ | [Dynamic Data Extraction Guide](docs/DYNAMIC_DATA_EXTRACTION_GUIDE.md) | 52 workflow state patterns for LLM agents |
808
+ | [Crawl4AI API Reference](docs/CRAWL4AI_ARCHITECTURE_REFERENCE.md) | Complete crawl4ai API surface reference |
809
+ | [Plugin Architecture](lib/plugin/ARCHITECTURE.md) | Plugin system design and lifecycle |
810
+ | [Configuration File](browsemind.yml) | YAML configuration reference |
811
+ | [Contributing Guidelines](CONTRIBUTING.md) | How to contribute to the project |
812
+ | [Security Policy](SECURITY.md) | Guidelines for reporting vulnerabilities |
813
+ | [Code of Conduct](CODE_OF_CONDUCT.md) | Community standards and expectations |
814
+
815
+ ---
816
+
817
+ ## 🙏 Acknowledgments
818
+
819
+ - **[Crawl4AI](https://github.com/unclecode/crawl4ai)** — The foundation web crawling library
820
+ - **[Browser-Use](https://github.com/browser-use/browser-use)** — Inspiration for DOM interaction patterns and CLI design
821
+ - **[Page-Agent](https://github.com/alibaba/page-agent)** — Inspiration for interactive element detection and indexing
822
+ - **[PixelRAG](https://github.com/StarTrail-org/PixelRAG)** — Inspiration for Queen Reader fast extraction and tile-based capture
823
+ - **[LiteLLM](https://github.com/BerriAI/litellm)** — Multi-provider LLM interface
824
+ - **[Playwright](https://playwright.dev/)** — Browser automation engine
825
+
826
+ ---
827
+
828
+ ## ⚖️ License
829
+
830
+ Copyright (c) 2026 BrowseMind Contributors. Licensed under the **GNU AGPL v3** — see [LICENSE](LICENSE) for details.
831
+
832
+ ---
833
+
834
+ ## Disclaimer
835
+
836
+ > [!CAUTION]
837
+ >
838
+ > This library is provided for **educational and research purposes only**. By using this library, you agree to comply with local and international data scraping and privacy laws. The authors and contributors are not responsible for any misuse of this software. Always respect the terms of service of websites and `robots.txt` files.
839
+
840
+ ---
841
+
842
+ ## 🎓 Citations
843
+
844
+ If you have used this library for research purposes, please cite us with the following references:
845
+
846
+ ### BrowseMind
847
+
848
+ **BibTeX:**
849
+ ```bibtex
850
+ @misc{browsemind,
851
+ author = {Prokopis3},
852
+ title = {BrowseMind: LLM Browser Automation Agent},
853
+ year = {2026},
854
+ publisher = {GitHub},
855
+ journal = {GitHub Repository},
856
+ url = {https://github.com/prokopis3/browsemind},
857
+ note = {An LLM browser automation \& web scraping agent powered by Crawl4AI}
858
+ }
859
+ ```
860
+
861
+ **Text citation:**
862
+ > Prokopis3 (2026). *BrowseMind: LLM Browser Automation Agent* [Computer software]. GitHub. https://github.com/prokopis3/browsemind
863
+
864
+ ### Crawl4AI (underlying engine)
865
+
866
+ **BibTeX:**
867
+ ```bibtex
868
+ @software{crawl4ai2024,
869
+ author = {UncleCode},
870
+ title = {Crawl4AI: Open-source LLM Friendly Web Crawler \& Scraper},
871
+ year = {2024},
872
+ publisher = {GitHub},
873
+ journal = {GitHub Repository},
874
+ howpublished = {\url{https://github.com/unclecode/crawl4ai}},
875
+ commit = {c66f3276fd355031c8632500911fe7041ad6fc14}
876
+ }
877
+ ```
878
+
879
+ ---
880
+
881
+ ## ☕ Support
882
+
883
+ If you find this project useful, consider buying me a coffee!
884
+
885
+ <div align="center">
886
+
887
+
888
+
889
+ <a href="https://www.buymeacoffee.com/prokopis" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" style="height: 60px !important;width: 217px !important;" ></a>
890
+ <br><br>
891
+
892
+ <a href="https://giphy.com/stickers/buy-me-a-coffee-support-thanks-for-your-hXMGQqJFlIQMOjpsKC" target="_blank">
893
+ <img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcWZpdGpuc2dkZjZoMmRoanRzNnhhcGtqcGl6eTU0dWcwd2dxcGk4MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/hXMGQqJFlIQMOjpsKC/giphy.gif" alt="Thank you — your support means a lot!" width="320">
894
+ </a>
895
+
896
+ </div>
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * BrowseMind — Node.js CLI Wrapper
5
+ * ===================================
6
+ * Thin wrapper that spawns the Python CLI. Enables npm/bun distribution
7
+ * while the Python backend handles all actual logic.
8
+ *
9
+ * Usage:
10
+ * browsemind <command> [options]
11
+ * crwl <command> [options]
12
+ *
13
+ * Postinstall:
14
+ * node bin/browsemind.js postinstall — first-time setup
15
+ */
16
+
17
+ const { spawn, execSync } = require("child_process");
18
+ const path = require("path");
19
+ const fs = require("fs");
20
+
21
+ const PACKAGE_NAME = "browsemind";
22
+ const CLI_NAME = "browsemind";
23
+
24
+ function findPython() {
25
+ // Try uv-managed Python first, then system Python
26
+ const candidates = ["uv run python", "python3", "python", "py -3"];
27
+ for (const py of candidates) {
28
+ try {
29
+ const [cmd, ...args] = py.split(" ");
30
+ execSync(`${cmd} ${args.join(" ")} --version`, { stdio: "ignore" });
31
+ return py;
32
+ } catch {
33
+ continue;
34
+ }
35
+ }
36
+ return "python";
37
+ }
38
+
39
+ function getProjectRoot() {
40
+ // Resolve the package root (where package.json and main.py live)
41
+ let dir = __dirname;
42
+ for (let i = 0; i < 5; i++) {
43
+ dir = path.resolve(dir, "..");
44
+ if (fs.existsSync(path.join(dir, "pyproject.toml"))) {
45
+ return dir;
46
+ }
47
+ }
48
+ // Fallback: assume we're in node_modules/.bin/ or bin/
49
+ return path.resolve(__dirname, "..");
50
+ }
51
+
52
+ function runPostinstall() {
53
+ const root = getProjectRoot();
54
+ console.log(`\n 📦 ${PACKAGE_NAME} v${require("../package.json").version}`);
55
+ console.log(` Setting up Python environment...`);
56
+
57
+ const py = findPython();
58
+ const mainPy = path.join(root, "main.py");
59
+
60
+ if (!fs.existsSync(mainPy)) {
61
+ console.error(` ❌ Could not find main.py at ${mainPy}`);
62
+ console.error(` Please install from source: git clone <repo> && cd ${PACKAGE_NAME}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ // Install Python dependencies
67
+ try {
68
+ console.log(` Installing Python dependencies...`);
69
+ if (py.startsWith("uv")) {
70
+ execSync("uv sync", { cwd: root, stdio: "inherit" });
71
+ } else {
72
+ execSync(`${py} -m pip install -e .`, { cwd: root, stdio: "inherit" });
73
+ }
74
+ } catch (e) {
75
+ console.error(` ⚠️ pip install failed: ${e.message}`);
76
+ console.log(` Run '${py} -m pip install -e .' manually.`);
77
+ }
78
+
79
+ // Install Playwright browsers
80
+ try {
81
+ console.log(` Installing Playwright browser (Chromium)...`);
82
+ execSync(`${py} -m playwright install chromium`, { cwd: root, stdio: "inherit" });
83
+ } catch {
84
+ console.log(` ⚠️ Playwright browser install skipped. Run 'playwright install chromium' if needed.`);
85
+ }
86
+
87
+ console.log(`\n ✅ ${PACKAGE_NAME} ready!`);
88
+ console.log(` Run 'browsemind doctor' to verify installation.`);
89
+ console.log(` Run 'browsemind --help' to see available commands.\n`);
90
+ }
91
+
92
+ function runCLI() {
93
+ const args = process.argv.slice(2);
94
+ const root = getProjectRoot();
95
+ const mainPy = path.join(root, "main.py");
96
+ const py = findPython();
97
+
98
+ if (!fs.existsSync(mainPy)) {
99
+ console.error(`❌ Could not find main.py at ${mainPy}`);
100
+ process.exit(1);
101
+ }
102
+
103
+ // Build the command based on Python runtime
104
+ let cmd, cmdArgs;
105
+ if (py.startsWith("uv")) {
106
+ cmd = "uv";
107
+ cmdArgs = ["run", "python", mainPy, ...args];
108
+ } else {
109
+ cmd = py;
110
+ cmdArgs = [mainPy, ...args];
111
+ }
112
+
113
+ const proc = spawn(cmd, cmdArgs, {
114
+ stdio: "inherit",
115
+ cwd: root,
116
+ env: { ...process.env, PYTHONUNBUFFERED: "1" },
117
+ });
118
+
119
+ proc.on("close", (code) => {
120
+ process.exit(code ?? 0);
121
+ });
122
+
123
+ proc.on("error", (err) => {
124
+ console.error(`❌ Failed to start: ${err.message}`);
125
+ process.exit(1);
126
+ });
127
+ }
128
+
129
+ // ── Entry ─────────────────────────────────────────────────────────────
130
+
131
+ if (process.argv[2] === "postinstall") {
132
+ runPostinstall();
133
+ } else {
134
+ runCLI();
135
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "browsemind",
3
+ "version": "0.5.0",
4
+ "description": "LLM browser automation agent — crawl, navigate, extract, interact",
5
+ "keywords": [
6
+ "browser-automation",
7
+ "web-scraping",
8
+ "crawling",
9
+ "ai-agent",
10
+ "llm",
11
+ "crawl4ai",
12
+ "web-crawler"
13
+ ],
14
+ "homepage": "https://github.com/prokopis3/browsemind",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/prokopis3/browsemind.git"
18
+ },
19
+ "license": "AGPL-3.0-or-later",
20
+ "author": "BrowseMind Contributors",
21
+ "bin": {
22
+ "browsemind": "./bin/browsemind.js"
23
+ },
24
+ "files": [
25
+ "bin/",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "preferGlobal": true,
33
+ "scripts": {
34
+ "postinstall": "node bin/browsemind.js postinstall",
35
+ "postupdate": "node bin/browsemind.js postinstall",
36
+ "prepare": "husky"
37
+ },
38
+ "lint-staged": {
39
+ "*.{py,pyw}": [
40
+ "uv tool run ruff check --fix --unsafe-fixes",
41
+ "uv tool run ruff format"
42
+ ],
43
+ "*.{yml,yaml,json,md}": [
44
+ "npx --no prettier --write"
45
+ ]
46
+ },
47
+ "devDependencies": {
48
+ "@commitlint/cli": "^21.1.0",
49
+ "@commitlint/config-conventional": "^21.1.0",
50
+ "husky": "^9.1.7",
51
+ "lint-staged": "^17.0.8",
52
+ "prettier": "^3.9.3"
53
+ }
54
+ }