tabman 0.1.0__tar.gz
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.
- tabman-0.1.0/LICENSE +21 -0
- tabman-0.1.0/PKG-INFO +16 -0
- tabman-0.1.0/README.md +141 -0
- tabman-0.1.0/setup.cfg +4 -0
- tabman-0.1.0/setup.py +26 -0
- tabman-0.1.0/tabman/__init__.py +0 -0
- tabman-0.1.0/tabman/categorizer.py +259 -0
- tabman-0.1.0/tabman/content_fetcher.py +156 -0
- tabman-0.1.0/tabman/main.py +75 -0
- tabman-0.1.0/tabman/tab_capture.py +60 -0
- tabman-0.1.0/tabman/tab_saver.py +151 -0
- tabman-0.1.0/tabman.egg-info/PKG-INFO +16 -0
- tabman-0.1.0/tabman.egg-info/SOURCES.txt +15 -0
- tabman-0.1.0/tabman.egg-info/dependency_links.txt +1 -0
- tabman-0.1.0/tabman.egg-info/entry_points.txt +2 -0
- tabman-0.1.0/tabman.egg-info/requires.txt +10 -0
- tabman-0.1.0/tabman.egg-info/top_level.txt +1 -0
tabman-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Anshul Gada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tabman-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tabman
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Home-page: https://github.com/Anshulgada/brave-tab-manager
|
|
5
|
+
Author: Anshul Gada
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: playwright
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
Requires-Dist: beautifulsoup4
|
|
10
|
+
Requires-Dist: google-generativeai
|
|
11
|
+
Requires-Dist: google-api-python-client
|
|
12
|
+
Requires-Dist: tenacity
|
|
13
|
+
Requires-Dist: openai
|
|
14
|
+
Requires-Dist: python-dotenv
|
|
15
|
+
Requires-Dist: ollama
|
|
16
|
+
Requires-Dist: markdown
|
tabman-0.1.0/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Tabman: Brave Tab Manager
|
|
2
|
+
|
|
3
|
+
A command-line tool designed to streamline your browsing experience by intelligently organizing your open tabs from the Brave browser. `tabman` helps you categorize, tag, and save your browsing sessions, making it easier to find and revisit websites later.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Tab Retrieval:** Fetches all open tabs from Brave using its remote debugging interface.
|
|
8
|
+
- **Intelligent Categorization:** Uses AI-powered language models to categorize your tabs:
|
|
9
|
+
- **Google Gemini:** Employs the Gemini 2.0 Flash model for advanced and fast categorization.
|
|
10
|
+
- **Mistral AI:** Leverages Mistral's large language models as an alternative to Gemini.
|
|
11
|
+
- **Ollama:** Enables local categorization using Ollama, supporting a variety of LLMs (e.g., Llama 2, Mistral).
|
|
12
|
+
- **Dynamic Tagging:** Generates a list of concise and relevant keywords to describe the content of each tab.
|
|
13
|
+
- **Flexible Saving:**
|
|
14
|
+
- Saves the tab data (title, URL, main category, and tags) as a JSON file, organized in date-based subfolders within the `data/` directory.
|
|
15
|
+
- Creates a Markdown version of each tab session, also stored in the date-based subfolders within the `data/` directory.
|
|
16
|
+
- Maintains a central `all_tabs.md` file in the `data/` directory, where all tab data is appended for easy access.
|
|
17
|
+
- **Secure API Key Management:** Utilizes a `.env` file to securely store API keys, which can be set using command-line arguments.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
1. **Clone the repository:**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/Anshulgada/brave-tab-manager.git
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
2. **Navigate to the project directory:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd Brave Tab Manager
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This assumes that your project folder is named `Brave Tab Manager`.
|
|
35
|
+
|
|
36
|
+
3. **Install `pipx` (if you don't have it):**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install --user pipx
|
|
40
|
+
python -m pipx ensurepath
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This ensures that `pipx` is installed globally and its executables are present in your system's PATH. You may need to restart your terminal for these changes to take effect.
|
|
44
|
+
|
|
45
|
+
4. **Install `tabman` using `pipx`:**
|
|
46
|
+
```bash
|
|
47
|
+
pipx install -e .
|
|
48
|
+
```
|
|
49
|
+
This command installs your tool into an isolated environment and makes the `tabman` command accessible from anywhere. Make sure you run this command from the root of the project directory, i.e. `Brave Tab Manager`.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
1. **Enable Remote Debugging in Brave:**
|
|
54
|
+
To allow `tabman` to connect with your Brave browser, you must enable remote debugging. Here's how:
|
|
55
|
+
|
|
56
|
+
- **Windows:**
|
|
57
|
+
1. Right-click on the Brave shortcut in your Start Menu or Taskbar and select "Properties."
|
|
58
|
+
2. In the "Target" field, add `--remote-debugging-port=9222` (or your preferred port number) to the end of the existing path, ensuring there's a space between the path and the flag. For example:
|
|
59
|
+
```
|
|
60
|
+
"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" --remote-debugging-port=9222
|
|
61
|
+
```
|
|
62
|
+
3. Click "Apply" and "OK."
|
|
63
|
+
- **macOS/Linux:** Modify the Brave shortcut (or application launcher) to include the `--remote-debugging-port=9222` flag in the command line that launches Brave. Consult your system's documentation for the specific method.
|
|
64
|
+
- After enabling remote debugging, make sure that you have restarted your Brave browser.
|
|
65
|
+
|
|
66
|
+
2. **Run the `tabman` tool:**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
tabman [OPTIONS]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
You can use the following options:
|
|
73
|
+
|
|
74
|
+
- `-c`, `--categorize`: Categorize your open tabs using the selected LLM model, and save the data. This is not necessary when setting the api keys.
|
|
75
|
+
- `-v`, `--version`: Display the version of `tabman` and exits.
|
|
76
|
+
- `-m`, `--model <MODEL>`: Specify the LLM model for categorization:
|
|
77
|
+
- `gemini` (default): Uses Google Gemini.
|
|
78
|
+
- `mistral`: Uses Mistral AI.
|
|
79
|
+
- `ollama`: Uses a local LLM with Ollama.
|
|
80
|
+
- `--save-keys`: Save the provided API keys to the `.env` file. Use this in combination with `-mk` and `-gk` options to save the keys.
|
|
81
|
+
- `-mk`, `--mistral-key <KEY>`: Provide your Mistral AI API key.
|
|
82
|
+
- `-gk`, `--gemini-key <KEY>`: Provide your Google Gemini API key.
|
|
83
|
+
- `-om`, `--ollama-model <MODEL>`: Provide the Ollama model name (e.g., `llama2`, `mistral`). Default is `llama2`.
|
|
84
|
+
|
|
85
|
+
**Examples:**
|
|
86
|
+
|
|
87
|
+
- **Categorize tabs using the default Gemini model:**
|
|
88
|
+
```bash
|
|
89
|
+
tabman -c
|
|
90
|
+
```
|
|
91
|
+
- **Categorize tabs using Mistral:**
|
|
92
|
+
```bash
|
|
93
|
+
tabman -c -m mistral
|
|
94
|
+
```
|
|
95
|
+
- **Categorize tabs using a specific Ollama model:**
|
|
96
|
+
```bash
|
|
97
|
+
tabman -c -m ollama -om llama3.2
|
|
98
|
+
```
|
|
99
|
+
- **Save API keys to the `.env` file:**
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
tabman --save-keys -gk YOUR_GEMINI_API_KEY -mk YOUR_MISTRAL_API_KEY
|
|
103
|
+
```
|
|
104
|
+
Replace `YOUR_GEMINI_API_KEY` and `YOUR_MISTRAL_API_KEY` with your actual API keys.
|
|
105
|
+
|
|
106
|
+
- **Show help message:**
|
|
107
|
+
```bash
|
|
108
|
+
tabman
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
- **API Keys:** API keys for Gemini and Mistral can be provided either directly via command-line arguments or saved to a `.env` file for later use. The tool will read keys from environment variables, and then the command line arguments and the `.env` file when using the tool.
|
|
114
|
+
- **Ollama:** If you select Ollama model, then you will have to install and run ollama on your system with your desired models.
|
|
115
|
+
|
|
116
|
+
## Development and Testing
|
|
117
|
+
|
|
118
|
+
- To avoid re-installing the package again and again, during local development, you should run the tool inside your virtual environment. For this you have to activate your environment and then run `pip install -e .` from the root directory. Now you can use the tool with the activated virtual environment. Whenever you want to update the global version then you have to uninstall the current version using `pipx uninstall tabman` and install again using `pipx install .`.
|
|
119
|
+
|
|
120
|
+
- **Running tests:**
|
|
121
|
+
- The tests are located in the `tabman/tests` directory. You can run the tests using `pytest` rom the root of your project i.e. where `setup.py` exists.
|
|
122
|
+
```bash
|
|
123
|
+
pytest -v -s tabman/tests
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Dependencies
|
|
127
|
+
|
|
128
|
+
- `openai`
|
|
129
|
+
- `ollama`
|
|
130
|
+
- `markdown`
|
|
131
|
+
- `requests`
|
|
132
|
+
- `tenacity`
|
|
133
|
+
- `playwright`
|
|
134
|
+
- `python-dotenv`
|
|
135
|
+
- `beautifulsoup4`
|
|
136
|
+
- `google-generativeai`
|
|
137
|
+
- `google-api-python-client`
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
This project is licensed under the MIT License.
|
tabman-0.1.0/setup.cfg
ADDED
tabman-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="tabman",
|
|
5
|
+
author="Anshul Gada",
|
|
6
|
+
version="0.1.0",
|
|
7
|
+
url="https://github.com/Anshulgada/brave-tab-manager",
|
|
8
|
+
packages=find_packages(),
|
|
9
|
+
install_requires=[
|
|
10
|
+
"playwright",
|
|
11
|
+
"requests",
|
|
12
|
+
"beautifulsoup4",
|
|
13
|
+
"google-generativeai",
|
|
14
|
+
"google-api-python-client",
|
|
15
|
+
"tenacity",
|
|
16
|
+
"openai",
|
|
17
|
+
"python-dotenv",
|
|
18
|
+
"ollama",
|
|
19
|
+
"markdown",
|
|
20
|
+
],
|
|
21
|
+
entry_points={
|
|
22
|
+
"console_scripts": [
|
|
23
|
+
"tabman = tabman.main:entry_point",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
import openai
|
|
4
|
+
import ollama
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import google.generativeai as genai
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from asyncio import TimeoutError
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
from typing import List, Dict, Optional
|
|
12
|
+
from .tab_capture import get_brave_tabs
|
|
13
|
+
from .content_fetcher import get_content_from_url
|
|
14
|
+
from .tab_saver import save_tabs_to_json, convert_json_to_markdown
|
|
15
|
+
from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Silence the google-generativeai logs, has to be done before the config import
|
|
19
|
+
logging.getLogger("google.generativeai").setLevel(logging.ERROR)
|
|
20
|
+
load_dotenv() # Load the environment variables
|
|
21
|
+
|
|
22
|
+
# GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
|
23
|
+
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
|
24
|
+
gemini_model = genai.GenerativeModel("gemini-2.0-flash-exp")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
MAIN_CATEGORIES = {
|
|
28
|
+
"YouTube": ["youtube.com", "youtu.be"],
|
|
29
|
+
"Coding": ["github.com", "stackoverflow.com"],
|
|
30
|
+
"Articles": [],
|
|
31
|
+
"Social Media": ["facebook.com", "twitter.com", "instagram.com"],
|
|
32
|
+
"News": [],
|
|
33
|
+
"Personal": [],
|
|
34
|
+
"Other": [],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_main_category(url: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Determines the main category of a URL based on predefined rules.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
url (str): The URL to analyze.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: The main category.
|
|
47
|
+
"""
|
|
48
|
+
parsed_url = urlparse(url)
|
|
49
|
+
netloc = parsed_url.netloc
|
|
50
|
+
|
|
51
|
+
for category, domains in MAIN_CATEGORIES.items():
|
|
52
|
+
for domain in domains:
|
|
53
|
+
if domain in netloc:
|
|
54
|
+
return category
|
|
55
|
+
return "Other"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def capitalize_tags(tags: List[str]) -> List[str]:
|
|
59
|
+
"""Capitalizes the first letter of each word in the tag.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
tags(list): list of tags
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
list: capitalized tags
|
|
66
|
+
"""
|
|
67
|
+
return [" ".join(word.capitalize() for word in tag.split()) for tag in tags]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def post_process_tags(tags: List[str]) -> List[str]:
|
|
71
|
+
"""
|
|
72
|
+
Post processes tags by removing empty string, duplicate tags, and extra text.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
tags(list): List of tags
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
list(str): A filtered list of tags.
|
|
79
|
+
"""
|
|
80
|
+
filtered_tags = [tag for tag in tags if tag] # Remove empty strings
|
|
81
|
+
filtered_tags = list(set(filtered_tags)) # Remove duplicates
|
|
82
|
+
filtered_tags = [
|
|
83
|
+
re.sub(
|
|
84
|
+
r"^(tag|tags|keyword|keywords|topic|topics|sub):?\s*",
|
|
85
|
+
"",
|
|
86
|
+
tag,
|
|
87
|
+
flags=re.IGNORECASE,
|
|
88
|
+
).strip()
|
|
89
|
+
for tag in filtered_tags
|
|
90
|
+
] # remove extra text
|
|
91
|
+
return capitalize_tags(filtered_tags)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@retry(
|
|
95
|
+
stop=stop_after_attempt(3),
|
|
96
|
+
wait=wait_fixed(2),
|
|
97
|
+
retry=retry_if_exception_type(TimeoutError),
|
|
98
|
+
)
|
|
99
|
+
async def generate_tags(
|
|
100
|
+
content: Optional[str],
|
|
101
|
+
main_category: str,
|
|
102
|
+
model_type: str = "gemini",
|
|
103
|
+
ollama_model: str = "llama3.2",
|
|
104
|
+
) -> Optional[List[str]]:
|
|
105
|
+
"""
|
|
106
|
+
Generates a list of tags for the given content, using the Gemini, Mistral, or Ollama API.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
content (str): The content to be tagged.
|
|
110
|
+
main_category (str): The main category of the content.
|
|
111
|
+
model_type(str): The model used to generate the tags (gemini or mistral or ollama)
|
|
112
|
+
ollama_model(str): The ollama model to use for generating tags
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
list: A list of tags (or None if tag generation fails).
|
|
116
|
+
"""
|
|
117
|
+
if not content:
|
|
118
|
+
return None
|
|
119
|
+
try:
|
|
120
|
+
prompt = f"""
|
|
121
|
+
Given the following text content:
|
|
122
|
+
{content}
|
|
123
|
+
And given that the main category is: {main_category}.
|
|
124
|
+
Generate a comma separated list of the most relevant and concise keywords to describe the content.
|
|
125
|
+
Do not use full sentences or phrases as tags.
|
|
126
|
+
Maximum 5 tags.
|
|
127
|
+
Only return the comma separated list of keywords. Do not add any extra text.
|
|
128
|
+
"""
|
|
129
|
+
if model_type == "gemini":
|
|
130
|
+
|
|
131
|
+
async def generate_with_timeout():
|
|
132
|
+
return await asyncio.to_thread(gemini_model.generate_content, prompt)
|
|
133
|
+
|
|
134
|
+
response = await asyncio.wait_for(
|
|
135
|
+
generate_with_timeout(), timeout=10
|
|
136
|
+
) # Run in thread to not block main thread and apply timeout
|
|
137
|
+
elif model_type == "mistral":
|
|
138
|
+
client = openai.OpenAI(api_key=os.getenv("MISTRAL_API_KEY"))
|
|
139
|
+
|
|
140
|
+
async def generate_with_timeout():
|
|
141
|
+
return await asyncio.to_thread(
|
|
142
|
+
client.chat.completions.create,
|
|
143
|
+
model="mistral-tiny",
|
|
144
|
+
messages=[{"role": "user", "content": prompt}],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
response = await asyncio.wait_for(generate_with_timeout(), timeout=10)
|
|
148
|
+
if response and response.choices:
|
|
149
|
+
return post_process_tags(
|
|
150
|
+
[
|
|
151
|
+
tag.strip()
|
|
152
|
+
for tag in response.choices[0].message.content.split(",")
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
return ["Other"]
|
|
157
|
+
elif model_type == "ollama":
|
|
158
|
+
|
|
159
|
+
async def generate_with_timeout():
|
|
160
|
+
return await asyncio.to_thread(
|
|
161
|
+
ollama.generate, model=ollama_model, prompt=prompt
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
response = await asyncio.wait_for(generate_with_timeout(), timeout=10)
|
|
165
|
+
if response and response["response"]:
|
|
166
|
+
return post_process_tags(
|
|
167
|
+
[tag.strip() for tag in response["response"].split(",")]
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
return ["Other"]
|
|
171
|
+
|
|
172
|
+
else:
|
|
173
|
+
return ["Other"]
|
|
174
|
+
if response and response.text:
|
|
175
|
+
return post_process_tags([tag.strip() for tag in response.text.split(",")])
|
|
176
|
+
else:
|
|
177
|
+
return ["Other"] # Return other if no proper response
|
|
178
|
+
except TimeoutError as e:
|
|
179
|
+
print(f"Error during tag generation, request timed out: {e}")
|
|
180
|
+
return None
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(f"Error during tag generation: {e}")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def categorize_tabs(
|
|
187
|
+
tabs: List[Dict[str, str]],
|
|
188
|
+
model_type: str = "gemini",
|
|
189
|
+
ollama_model: str = "llama3.2",
|
|
190
|
+
) -> List[Dict[str, str]]:
|
|
191
|
+
"""
|
|
192
|
+
Categorizes a list of tabs based on their content.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
tabs (list): A list of dictionaries, each with 'title' and 'url' keys.
|
|
196
|
+
model_type(str): The model used to generate the tags (gemini or mistral or ollama)
|
|
197
|
+
ollama_model(str): The ollama model to use for generating tags
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
list: A list of dictionaries, each with 'title', 'url', 'main_category', and 'tags' keys.
|
|
201
|
+
"""
|
|
202
|
+
categorized_tabs = []
|
|
203
|
+
for tab in tabs:
|
|
204
|
+
print(f"Categorizing tab: {tab['title']}")
|
|
205
|
+
url = tab["url"]
|
|
206
|
+
main_category = get_main_category(url)
|
|
207
|
+
content = await asyncio.to_thread(get_content_from_url, url)
|
|
208
|
+
if content:
|
|
209
|
+
tags = await generate_tags(content, main_category, model_type, ollama_model)
|
|
210
|
+
tab["main_category"] = main_category
|
|
211
|
+
tab["tags"] = tags if tags else ["Other"]
|
|
212
|
+
else:
|
|
213
|
+
tab["main_category"] = "Other"
|
|
214
|
+
tab["tags"] = ["Other"]
|
|
215
|
+
|
|
216
|
+
categorized_tabs.append(tab)
|
|
217
|
+
return categorized_tabs
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def main_categorizer(
|
|
221
|
+
model_type: str = "gemini",
|
|
222
|
+
save_keys: bool = False,
|
|
223
|
+
mistral_key=None,
|
|
224
|
+
gemini_key=None,
|
|
225
|
+
ollama_model="llama3.2",
|
|
226
|
+
):
|
|
227
|
+
"""
|
|
228
|
+
Main function for categorizing and saving tabs
|
|
229
|
+
Args:
|
|
230
|
+
model_type(str): The model used to generate the tags (gemini or mistral or ollama)
|
|
231
|
+
save_keys (bool): If we want to save the keys
|
|
232
|
+
mistral_key (str): Mistral api key if we want to set using command line args
|
|
233
|
+
gemini_key (str): Gemini api key if we want to set using command line args
|
|
234
|
+
ollama_model (str): The ollama model to use for generating tags
|
|
235
|
+
"""
|
|
236
|
+
if save_keys:
|
|
237
|
+
if gemini_key:
|
|
238
|
+
os.environ["GEMINI_API_KEY"] = gemini_key
|
|
239
|
+
if mistral_key:
|
|
240
|
+
os.environ["MISTRAL_API_KEY"] = mistral_key
|
|
241
|
+
|
|
242
|
+
tabs = await get_brave_tabs()
|
|
243
|
+
if tabs:
|
|
244
|
+
categorized_tabs = await categorize_tabs(tabs, model_type, ollama_model)
|
|
245
|
+
json_file = save_tabs_to_json(categorized_tabs)
|
|
246
|
+
print(f"Saved tabs to JSON: {json_file}")
|
|
247
|
+
|
|
248
|
+
markdown_file = convert_json_to_markdown(json_file)
|
|
249
|
+
print(f"Converted JSON to markdown: {markdown_file}")
|
|
250
|
+
else:
|
|
251
|
+
print("Could not get the tabs from brave browser.")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
|
|
256
|
+
async def main():
|
|
257
|
+
await main_categorizer()
|
|
258
|
+
|
|
259
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from googleapiclient.discovery import build
|
|
6
|
+
from googleapiclient.errors import HttpError
|
|
7
|
+
|
|
8
|
+
# from config import GEMINI_API_KEY
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
YOUTUBE_API_KEY = os.getenv(
|
|
14
|
+
"GEMINI_API_KEY"
|
|
15
|
+
) # For using Gemini API, reuse this api key
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_limited_content(html_content: str) -> Optional[str]:
|
|
19
|
+
"""
|
|
20
|
+
Extracts limited key text content from HTML (title, meta description, and initial content).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
html_content (str): The HTML content as a string.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
str: The extracted text content, or None if extraction fails.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
30
|
+
|
|
31
|
+
title = soup.title.string if soup.title else ""
|
|
32
|
+
meta_description = soup.find("meta", attrs={"name": "description"})
|
|
33
|
+
meta_description = (
|
|
34
|
+
meta_description.get("content", "") if meta_description else ""
|
|
35
|
+
)
|
|
36
|
+
main_content_elements = soup.find_all(["p", "h1", "h2", "h3", "h4"])
|
|
37
|
+
main_content = " ".join(
|
|
38
|
+
element.get_text(separator=" ", strip=True)
|
|
39
|
+
for element in main_content_elements[:5]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return f"{title}. {meta_description}. {main_content}"
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Error extracting content from HTML: {e}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch_url_content(url: str) -> Optional[str]:
|
|
49
|
+
"""
|
|
50
|
+
Fetches the HTML content from a given URL.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
url (str): The URL to fetch.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: The HTML content or None if the request fails.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
response = requests.get(url, timeout=10)
|
|
60
|
+
response.raise_for_status() # Raise an exception for bad status codes
|
|
61
|
+
return response.text
|
|
62
|
+
except requests.exceptions.RequestException as e:
|
|
63
|
+
print(f"Error fetching URL {url}: {e}")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def extract_youtube_data(video_id: str) -> Optional[str]:
|
|
68
|
+
"""
|
|
69
|
+
Extracts the title and description from a YouTube video.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
video_id (str): The YouTube video ID.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: The title and description of the youtube video or None if something goes wrong.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
youtube = build("youtube", "v3", developerKey=YOUTUBE_API_KEY)
|
|
79
|
+
request = youtube.videos().list(part="snippet", id=video_id)
|
|
80
|
+
response = request.execute()
|
|
81
|
+
if not response or "items" not in response or not response["items"]:
|
|
82
|
+
return None
|
|
83
|
+
video_data = response["items"][0]["snippet"]
|
|
84
|
+
title = video_data.get("title", "")
|
|
85
|
+
description = video_data.get("description", "")
|
|
86
|
+
return f"{title}. {description}"
|
|
87
|
+
except HttpError as e:
|
|
88
|
+
print(f"An HTTP error {e.resp.status} occurred:\n{e.content}")
|
|
89
|
+
return None
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"An Error occurred: {e}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_content_from_url(url: str) -> Optional[str]:
|
|
96
|
+
"""
|
|
97
|
+
Helper function to extract content from both normal urls and youtube urls.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
url (str): url to get content from
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
str: The content of the url or None.
|
|
104
|
+
"""
|
|
105
|
+
if "youtube.com" in url or "youtu.be" in url:
|
|
106
|
+
video_id = get_youtube_id(url)
|
|
107
|
+
if video_id:
|
|
108
|
+
return extract_youtube_data(video_id)
|
|
109
|
+
return None
|
|
110
|
+
html_content = fetch_url_content(url)
|
|
111
|
+
if html_content:
|
|
112
|
+
return extract_limited_content(
|
|
113
|
+
html_content
|
|
114
|
+
) # Changed this to extract limited content
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_youtube_id(url: str) -> Optional[str]:
|
|
119
|
+
"""
|
|
120
|
+
Extracts the YouTube video ID from a URL.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
url (str): The URL of the YouTube video.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
str: The video ID or None if not found
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
if "youtube.com" in url:
|
|
130
|
+
from urllib.parse import urlparse, parse_qs
|
|
131
|
+
|
|
132
|
+
parsed_url = urlparse(url)
|
|
133
|
+
if parsed_url.query:
|
|
134
|
+
query_params = parse_qs(parsed_url.query)
|
|
135
|
+
if "v" in query_params:
|
|
136
|
+
return query_params["v"][0]
|
|
137
|
+
if parsed_url.path.startswith("/watch/"):
|
|
138
|
+
return parsed_url.path.split("/")[2]
|
|
139
|
+
elif "youtu.be" in url:
|
|
140
|
+
from urllib.parse import urlparse
|
|
141
|
+
|
|
142
|
+
parsed_url = urlparse(url)
|
|
143
|
+
return parsed_url.path[1:]
|
|
144
|
+
return None
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"Error extracting youtube id: {e}")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
sample_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
152
|
+
content = get_content_from_url(sample_url)
|
|
153
|
+
if content:
|
|
154
|
+
print(f"Content of url : {content[:200]} ...")
|
|
155
|
+
else:
|
|
156
|
+
print("Error fetching content")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import argparse
|
|
4
|
+
from dotenv import load_dotenv, set_key
|
|
5
|
+
from .categorizer import main_categorizer
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def main():
|
|
11
|
+
# Create argument parser for CLI usage
|
|
12
|
+
parser = argparse.ArgumentParser(description="Brave Tab Manager CLI Tool")
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"-c",
|
|
15
|
+
"--categorize",
|
|
16
|
+
action="store_true",
|
|
17
|
+
help="Categorize and save current open tabs.",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("-v", "--version", action="version", version="%(prog)s 0.1.0")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-m",
|
|
22
|
+
"--model",
|
|
23
|
+
type=str,
|
|
24
|
+
choices=["gemini", "mistral", "ollama"],
|
|
25
|
+
default="gemini",
|
|
26
|
+
help="Specify the LLM model for categorization",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--save-keys",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Save API keys to the .env file. Use this option with -gk or -mk to set the keys.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("-mk", "--mistral-key", type=str, help="Mistral AI API key.")
|
|
34
|
+
parser.add_argument("-gk", "--gemini-key", type=str, help="Google Gemini API key.")
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-om",
|
|
37
|
+
"--ollama-model",
|
|
38
|
+
type=str,
|
|
39
|
+
default="llama3.2",
|
|
40
|
+
help="Ollama model to use for categorization (default: llama3.2)",
|
|
41
|
+
)
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
# If we're saving keys, update environment and .env file
|
|
45
|
+
if args.save_keys:
|
|
46
|
+
env_path = ".env"
|
|
47
|
+
if args.gemini_key:
|
|
48
|
+
os.environ["GEMINI_API_KEY"] = args.gemini_key
|
|
49
|
+
set_key(env_path, "GEMINI_API_KEY", args.gemini_key)
|
|
50
|
+
print("Gemini key has been set")
|
|
51
|
+
if args.mistral_key:
|
|
52
|
+
os.environ["MISTRAL_API_KEY"] = args.mistral_key
|
|
53
|
+
set_key(env_path, "MISTRAL_API_KEY", args.mistral_key)
|
|
54
|
+
print("Mistral key has been set")
|
|
55
|
+
|
|
56
|
+
# Run categorization if requested
|
|
57
|
+
if args.categorize:
|
|
58
|
+
await main_categorizer(
|
|
59
|
+
args.model,
|
|
60
|
+
args.save_keys,
|
|
61
|
+
args.mistral_key,
|
|
62
|
+
args.gemini_key,
|
|
63
|
+
args.ollama_model,
|
|
64
|
+
)
|
|
65
|
+
elif not args.save_keys and not getattr(args, "version", False):
|
|
66
|
+
parser.print_help()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def entry_point():
|
|
70
|
+
# Wrap async main in a synchronous entry point
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
entry_point()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from playwright.async_api import async_playwright
|
|
3
|
+
|
|
4
|
+
BRAVE_DEBUG_PORT = 9222 # Define the debug port here
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def get_browser_tabs(browser_url: str):
|
|
8
|
+
"""
|
|
9
|
+
Retrieves a list of open tabs from the browser.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
browser_url (str): The browser's http url.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
list[dict]: A list of dictionaries, each containing 'title' and 'url' keys.
|
|
16
|
+
Returns empty list if it fails to connect to browser.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
async with async_playwright() as p:
|
|
20
|
+
browser = await p.chromium.connect_over_cdp(browser_url)
|
|
21
|
+
|
|
22
|
+
tabs = []
|
|
23
|
+
for context in browser.contexts: # Iterate through contexts
|
|
24
|
+
for page in context.pages: # Iterate through pages in each context
|
|
25
|
+
title = await page.title()
|
|
26
|
+
url = page.url
|
|
27
|
+
tabs.append({"title": title, "url": url})
|
|
28
|
+
await browser.close()
|
|
29
|
+
return tabs
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"Failed to get browser tabs: {e}")
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def get_brave_tabs():
|
|
36
|
+
"""
|
|
37
|
+
Helper function to get brave browser's http url using debug port.
|
|
38
|
+
Returns:
|
|
39
|
+
list[dict]: A list of dictionaries, each containing 'title' and 'url' keys.
|
|
40
|
+
Returns empty list if it fails to get browser tabs.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
browser_url = f"http://127.0.0.1:{BRAVE_DEBUG_PORT}"
|
|
44
|
+
return await get_browser_tabs(browser_url)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"Failed to get browser tabs: {e}")
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
|
|
52
|
+
async def main():
|
|
53
|
+
tabs = await get_brave_tabs()
|
|
54
|
+
if tabs:
|
|
55
|
+
for tab in tabs:
|
|
56
|
+
print(f"Title: {tab['title']}, URL: {tab['url']}")
|
|
57
|
+
else:
|
|
58
|
+
print("No tabs found.")
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
|
|
7
|
+
# Silence the google-generativeai logs, has to be done before the config import
|
|
8
|
+
logging.getLogger("google.generativeai").setLevel(logging.ERROR)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def save_tabs_to_json(tabs: List[Dict[str, str]], data_dir: str = "data") -> str:
|
|
12
|
+
"""
|
|
13
|
+
Saves tab data to a JSON file.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
tabs (list): A list of dictionaries, each with 'title', 'url', 'main_category', and 'tags' keys.
|
|
17
|
+
data_dir (str): The directory to save the JSON file in
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
str: The path to the saved JSON file
|
|
21
|
+
"""
|
|
22
|
+
now = datetime.now()
|
|
23
|
+
date_str = now.strftime("%d-%m-%Y")
|
|
24
|
+
time_str = now.strftime("%H-%M-%S")
|
|
25
|
+
day_str = now.strftime("%A")
|
|
26
|
+
filename = f"tabs_{date_str}_{day_str}_{time_str}.json"
|
|
27
|
+
|
|
28
|
+
dated_dir = os.path.join(data_dir, date_str)
|
|
29
|
+
filepath = os.path.join(dated_dir, filename)
|
|
30
|
+
|
|
31
|
+
os.makedirs(dated_dir, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
34
|
+
json.dump(tabs, f, indent=4)
|
|
35
|
+
|
|
36
|
+
all_tabs_filepath = os.path.join(data_dir, "all_tabs.md")
|
|
37
|
+
append_tabs_to_all_markdown(tabs, all_tabs_filepath)
|
|
38
|
+
return filepath
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def append_tabs_to_all_markdown(tabs: List[Dict[str, str]], filepath: str):
|
|
42
|
+
"""
|
|
43
|
+
Appends tab data to the central all_tabs.md file.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
tabs(list): A list of dictionaries, each with 'title', 'url', 'main_category', and 'tags' keys.
|
|
47
|
+
filepath(str): The path of the all_tabs.md file
|
|
48
|
+
"""
|
|
49
|
+
with open(filepath, "a", encoding="utf-8") as f:
|
|
50
|
+
f.write("# Browser Tab Data\n\n")
|
|
51
|
+
for tab in tabs:
|
|
52
|
+
title = tab.get("title", "No Title")
|
|
53
|
+
url = tab.get("url", "No URL")
|
|
54
|
+
main_category = tab.get("main_category", "Other")
|
|
55
|
+
tags = tab.get("tags", [])
|
|
56
|
+
|
|
57
|
+
f.write(f"## {title}\n")
|
|
58
|
+
f.write(f"- **URL:** {url}\n")
|
|
59
|
+
f.write(f"- **Main Category:** {main_category}\n")
|
|
60
|
+
f.write(f"- **Tags:** {', '.join(tags)}\n\n")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def convert_json_to_markdown(json_filepath: str, data_dir: str = "data") -> str:
|
|
64
|
+
"""
|
|
65
|
+
Converts the saved JSON data to markdown format.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
json_filepath(str): The path of the json file
|
|
69
|
+
data_dir(str): The directory where data is stored (used to get the markdown output folder path)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
str: The path of the created markdown file
|
|
73
|
+
"""
|
|
74
|
+
with open(json_filepath, "r", encoding="utf-8") as f:
|
|
75
|
+
tabs = json.load(f)
|
|
76
|
+
|
|
77
|
+
now = datetime.now()
|
|
78
|
+
date_str = now.strftime("%d-%m-%Y")
|
|
79
|
+
time_str = now.strftime("%H-%M-%S")
|
|
80
|
+
day_str = now.strftime("%A")
|
|
81
|
+
markdown_dir = os.path.join(data_dir, date_str)
|
|
82
|
+
os.makedirs(markdown_dir, exist_ok=True)
|
|
83
|
+
markdown_file = os.path.join(
|
|
84
|
+
markdown_dir, f"tabs_{date_str}_{day_str}_{time_str}.md"
|
|
85
|
+
)
|
|
86
|
+
with open(markdown_file, "w", encoding="utf-8") as f:
|
|
87
|
+
f.write(f"# Tabs - {date_str} ({day_str})\n\n")
|
|
88
|
+
for tab in tabs:
|
|
89
|
+
title = tab.get("title", "No Title")
|
|
90
|
+
url = tab.get("url", "No URL")
|
|
91
|
+
main_category = tab.get("main_category", "Other")
|
|
92
|
+
tags = tab.get("tags", [])
|
|
93
|
+
|
|
94
|
+
f.write(f"## {title}\n")
|
|
95
|
+
f.write(f"- **URL:** {url}\n")
|
|
96
|
+
f.write(f"- **Main Category:** {main_category}\n")
|
|
97
|
+
f.write(f"- **Tags:** {', '.join(tags)}\n\n")
|
|
98
|
+
return markdown_file
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
|
|
103
|
+
# Testing the functions
|
|
104
|
+
sample_tabs = [
|
|
105
|
+
{
|
|
106
|
+
"title": "Playwright Docs",
|
|
107
|
+
"url": "https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp",
|
|
108
|
+
"main_category": "Other",
|
|
109
|
+
"tags": [
|
|
110
|
+
"Playwright",
|
|
111
|
+
"Browsertype",
|
|
112
|
+
"Connect",
|
|
113
|
+
"Browser Instance",
|
|
114
|
+
"Automation",
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"title": "Rick Astley - Never Gonna Give You Up",
|
|
119
|
+
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
120
|
+
"main_category": "YouTube",
|
|
121
|
+
"tags": [
|
|
122
|
+
"Music Video",
|
|
123
|
+
"Rick Astley",
|
|
124
|
+
"Official",
|
|
125
|
+
"Never Gonna Give You Up",
|
|
126
|
+
"1980s",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"title": "Brave Tab Manager: Design Plan",
|
|
131
|
+
"url": "https://aistudio.google.com/u/3/prompts/1SyHQU8Re8fJ8Cga95N5jG5ptRmvV8BoL",
|
|
132
|
+
"main_category": "Other",
|
|
133
|
+
"tags": [
|
|
134
|
+
"Sign In",
|
|
135
|
+
"Guest Mode",
|
|
136
|
+
"Private Browsing",
|
|
137
|
+
"Forgot Email",
|
|
138
|
+
"Google Accounts",
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"title": "Github OAuth",
|
|
143
|
+
"url": "https://github.com/login/oauth/authorize?client_id=01ab8ac9400c4e429b23&redirect_uri=https%3A%2F%2Fvscode.dev%2Fredirect&scope=user%3Aemail&skip_account_picker=true&state=vscode%253A%252F%2Fvscode.github-authentication%252Fdid-authenticate%253Fnonce%253Db9147fedcf4304bb%2526windowId%253D1",
|
|
144
|
+
"main_category": "Coding",
|
|
145
|
+
"tags": ["Projects", "Software", "Passkey", "Coding", "Github"],
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
json_file = save_tabs_to_json(sample_tabs)
|
|
149
|
+
print(f"Saved tabs to JSON: {json_file}")
|
|
150
|
+
markdown_file = convert_json_to_markdown(json_file)
|
|
151
|
+
print(f"Converted JSON to markdown: {markdown_file}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tabman
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Home-page: https://github.com/Anshulgada/brave-tab-manager
|
|
5
|
+
Author: Anshul Gada
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: playwright
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
Requires-Dist: beautifulsoup4
|
|
10
|
+
Requires-Dist: google-generativeai
|
|
11
|
+
Requires-Dist: google-api-python-client
|
|
12
|
+
Requires-Dist: tenacity
|
|
13
|
+
Requires-Dist: openai
|
|
14
|
+
Requires-Dist: python-dotenv
|
|
15
|
+
Requires-Dist: ollama
|
|
16
|
+
Requires-Dist: markdown
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
tabman/__init__.py
|
|
5
|
+
tabman/categorizer.py
|
|
6
|
+
tabman/content_fetcher.py
|
|
7
|
+
tabman/main.py
|
|
8
|
+
tabman/tab_capture.py
|
|
9
|
+
tabman/tab_saver.py
|
|
10
|
+
tabman.egg-info/PKG-INFO
|
|
11
|
+
tabman.egg-info/SOURCES.txt
|
|
12
|
+
tabman.egg-info/dependency_links.txt
|
|
13
|
+
tabman.egg-info/entry_points.txt
|
|
14
|
+
tabman.egg-info/requires.txt
|
|
15
|
+
tabman.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tabman
|