mirror of
https://github.com/tcsenpai/suggestoor.git
synced 2025-06-06 03:05:33 +00:00
first commit
This commit is contained in:
commit
775bfa7c56
13
.env.example
Normal file
13
.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FOLDERS=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
|
||||||
|
|
||||||
|
# OLLAMA settings
|
||||||
|
OLLAMA_HOST=http://localhost:11434
|
||||||
|
# NOTE On the model: you should use this model as it's tested
|
||||||
|
# or models with higher params than 8b anyway.
|
||||||
|
# You can download this model with:
|
||||||
|
# ollama pull llama3.1:8b-instruct-q4_K_M
|
||||||
|
OLLAMA_MODEL=llama3.1:8b-instruct-q4_K_M
|
||||||
|
EMBEDDING_MODEL=nomic-embed-text
|
||||||
|
|
||||||
|
# Vector search parameters
|
||||||
|
SIMILARITY_K=8 # Number of similar chunks to retrieve
|
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Faiss
|
||||||
|
vector_store/
|
||||||
|
mans/
|
||||||
|
embedding_progress.json
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.11
|
66
README.md
Normal file
66
README.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Suggestoor
|
||||||
|
|
||||||
|
Suggestoor is a tool that helps you find the right command-line tools for your needs.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
At the first run, it will take a while to embed all the man pages.
|
||||||
|
It can take up to 1-2 hours.
|
||||||
|
This is a one-time operation.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- ollama server running
|
||||||
|
- a GPU for embedding (using a CPU is unfeasible)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Using uv
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using system python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python suggestoor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Using uv
|
||||||
|
|
||||||
|
Just run the script and it will install the dependencies for you.
|
||||||
|
|
||||||
|
### Using pip
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
You can configure the tool by editing the `.env` file.
|
||||||
|
|
||||||
|
### OLLAMA_HOST
|
||||||
|
|
||||||
|
This is the host of the ollama server.
|
||||||
|
|
||||||
|
### OLLAMA_MODEL
|
||||||
|
|
||||||
|
This is the model that will be used to generate the response.
|
||||||
|
|
||||||
|
### EMBEDDING_MODEL
|
||||||
|
|
||||||
|
This is the model that will be used to embed the man pages.
|
||||||
|
|
||||||
|
### SIMILARITY_K
|
||||||
|
|
||||||
|
This is the number of similar chunks to retrieve. Increase this number if you want more context. For a 8gb ram machine, 8 to 12 is a good number.
|
||||||
|
|
||||||
|
### FOLDERS
|
||||||
|
|
||||||
|
This is the folders of binaries to search for man pages and embed them.
|
225
libs/get_all_mans.py
Normal file
225
libs/get_all_mans.py
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
# This script will get all the man pages from the binaries and save them in a file
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import dotenv
|
||||||
|
import json
|
||||||
|
from langchain_ollama import OllamaEmbeddings
|
||||||
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
|
from langchain_community.vectorstores import FAISS
|
||||||
|
from colorama import Fore, Style
|
||||||
|
from tqdm import tqdm
|
||||||
|
import sys
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
def get_binaries():
|
||||||
|
binaries = []
|
||||||
|
folders = os.getenv("FOLDERS").split(":")
|
||||||
|
for folder in folders:
|
||||||
|
for file in os.listdir(folder):
|
||||||
|
binaries.append(file)
|
||||||
|
return binaries
|
||||||
|
|
||||||
|
|
||||||
|
def load_progress():
|
||||||
|
"""Load the progress of already processed binaries"""
|
||||||
|
try:
|
||||||
|
if os.path.exists("embedding_progress.json"):
|
||||||
|
with open("embedding_progress.json", "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Could not load progress, starting fresh: {e}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
return {"processed_binaries": [], "vector_store_exists": False}
|
||||||
|
|
||||||
|
|
||||||
|
def save_progress(processed_binaries, vector_store_exists=False):
|
||||||
|
"""Save the progress of processed binaries"""
|
||||||
|
try:
|
||||||
|
with open("embedding_progress.json", "w") as f:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"processed_binaries": processed_binaries,
|
||||||
|
"vector_store_exists": vector_store_exists,
|
||||||
|
},
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Fore.YELLOW}Could not save progress: {e}{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_embeddings_db(texts, metadatas, existing_db=None):
|
||||||
|
vector_store_path = os.path.abspath("vector_store")
|
||||||
|
print(
|
||||||
|
f"{Fore.CYAN}Will create/update vector store at: {vector_store_path}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"{Fore.CYAN}Creating embeddings...{Style.RESET_ALL}")
|
||||||
|
embeddings = OllamaEmbeddings(
|
||||||
|
model=os.getenv("EMBEDDING_MODEL"),
|
||||||
|
base_url=os.getenv("OLLAMA_HOST"),
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_size = 64
|
||||||
|
total_batches = (len(texts) + batch_size - 1) // batch_size
|
||||||
|
|
||||||
|
all_embeddings = []
|
||||||
|
with tqdm(
|
||||||
|
total=len(texts),
|
||||||
|
desc="Creating vectors",
|
||||||
|
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
|
||||||
|
) as pbar:
|
||||||
|
for i in range(0, len(texts), batch_size):
|
||||||
|
batch = texts[i : i + batch_size]
|
||||||
|
try:
|
||||||
|
batch_embeddings = embeddings.embed_documents(batch)
|
||||||
|
all_embeddings.extend(batch_embeddings)
|
||||||
|
pbar.update(len(batch))
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"{Fore.RED}Error creating embeddings for batch {i//batch_size + 1}/{total_batches}: {e}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
print(f"{Fore.CYAN}Creating/updating FAISS index...{Style.RESET_ALL}")
|
||||||
|
try:
|
||||||
|
if existing_db is None:
|
||||||
|
db = FAISS.from_embeddings(
|
||||||
|
text_embeddings=list(zip(texts, all_embeddings)),
|
||||||
|
embedding=embeddings,
|
||||||
|
metadatas=metadatas,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
db = existing_db
|
||||||
|
db.add_embeddings(
|
||||||
|
text_embeddings=list(zip(texts, all_embeddings)), metadatas=metadatas
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{Fore.CYAN}Saving vector store to: {vector_store_path}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
db.save_local(vector_store_path)
|
||||||
|
if os.path.exists(vector_store_path):
|
||||||
|
print(
|
||||||
|
f"{Fore.GREEN}Vector store successfully saved at: {vector_store_path}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"{Fore.CYAN}Vector store size: {sum(os.path.getsize(os.path.join(vector_store_path, f)) for f in os.listdir(vector_store_path)) / (1024*1024):.2f} MB{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
return db
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Fore.RED}Error with FAISS index: {e}{Style.RESET_ALL}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_mans():
|
||||||
|
# Load previous progress
|
||||||
|
progress = load_progress()
|
||||||
|
processed_binaries = progress["processed_binaries"]
|
||||||
|
|
||||||
|
binaries = get_binaries()
|
||||||
|
remaining_binaries = [b for b in binaries if b not in processed_binaries]
|
||||||
|
|
||||||
|
if not remaining_binaries:
|
||||||
|
print(f"{Fore.GREEN}All binaries already processed!{Style.RESET_ALL}")
|
||||||
|
if progress["vector_store_exists"]:
|
||||||
|
embeddings = OllamaEmbeddings(
|
||||||
|
model=os.getenv("EMBEDDING_MODEL"),
|
||||||
|
base_url=os.getenv("OLLAMA_HOST"),
|
||||||
|
)
|
||||||
|
return FAISS.load_local("vector_store", embeddings)
|
||||||
|
|
||||||
|
texts = []
|
||||||
|
metadatas = []
|
||||||
|
|
||||||
|
# Load existing vector store if it exists
|
||||||
|
existing_db = None
|
||||||
|
if progress["vector_store_exists"]:
|
||||||
|
try:
|
||||||
|
embeddings = OllamaEmbeddings(
|
||||||
|
model=os.getenv("EMBEDDING_MODEL"),
|
||||||
|
base_url=os.getenv("OLLAMA_HOST"),
|
||||||
|
)
|
||||||
|
existing_db = FAISS.load_local("vector_store", embeddings)
|
||||||
|
print(f"{Fore.GREEN}Loaded existing vector store{Style.RESET_ALL}")
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Could not load existing vector store: {e}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text splitter for chunking
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", " ", ""]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"{Fore.CYAN}Processing remaining man pages...{Style.RESET_ALL}")
|
||||||
|
try:
|
||||||
|
for binary in tqdm(
|
||||||
|
remaining_binaries,
|
||||||
|
desc="Reading man pages",
|
||||||
|
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
|
||||||
|
):
|
||||||
|
man_content = ""
|
||||||
|
if os.path.exists(f"mans/{binary}.man"):
|
||||||
|
with open(f"mans/{binary}.man", "r") as file:
|
||||||
|
man_content = file.read()
|
||||||
|
else:
|
||||||
|
man_page = subprocess.run(
|
||||||
|
["man", binary], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
man_content = man_page.stdout
|
||||||
|
with open(f"mans/{binary}.man", "w") as file:
|
||||||
|
file.write(man_content)
|
||||||
|
|
||||||
|
if man_content.strip():
|
||||||
|
chunks = text_splitter.split_text(man_content)
|
||||||
|
texts.extend(chunks)
|
||||||
|
metadatas.extend(
|
||||||
|
[
|
||||||
|
{"binary": binary, "chunk": i, "total_chunks": len(chunks)}
|
||||||
|
for i in range(len(chunks))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save progress after each binary
|
||||||
|
processed_binaries.append(binary)
|
||||||
|
save_progress(processed_binaries, progress["vector_store_exists"])
|
||||||
|
|
||||||
|
# Create embeddings in smaller batches
|
||||||
|
if len(texts) >= 100: # Process every 100 documents
|
||||||
|
db = create_embeddings_db(texts, metadatas, existing_db)
|
||||||
|
existing_db = db
|
||||||
|
texts = []
|
||||||
|
metadatas = []
|
||||||
|
progress["vector_store_exists"] = True
|
||||||
|
save_progress(processed_binaries, True)
|
||||||
|
|
||||||
|
# Process any remaining texts
|
||||||
|
if texts:
|
||||||
|
db = create_embeddings_db(texts, metadatas, existing_db)
|
||||||
|
elif existing_db:
|
||||||
|
db = existing_db
|
||||||
|
|
||||||
|
save_progress(processed_binaries, True)
|
||||||
|
return db
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(
|
||||||
|
f"\n{Fore.YELLOW}Process interrupted! Progress has been saved.{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
if texts:
|
||||||
|
print(f"{Fore.YELLOW}Saving current batch...{Style.RESET_ALL}")
|
||||||
|
try:
|
||||||
|
db = create_embeddings_db(texts, metadatas, existing_db)
|
||||||
|
save_progress(processed_binaries, True)
|
||||||
|
print(f"{Fore.GREEN}Current batch saved successfully!{Style.RESET_ALL}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Fore.RED}Could not save current batch: {e}{Style.RESET_ALL}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
db = get_all_mans()
|
||||||
|
print(f"{Fore.GREEN}✓ Created embeddings database{Style.RESET_ALL}")
|
69
libs/ollama_client.py
Normal file
69
libs/ollama_client.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import os
|
||||||
|
import dotenv
|
||||||
|
from ollama import Client
|
||||||
|
from langchain_community.vectorstores import FAISS
|
||||||
|
from langchain_ollama import OllamaEmbeddings
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
def load_vector_store():
|
||||||
|
embeddings = OllamaEmbeddings(
|
||||||
|
model=os.getenv("EMBEDDING_MODEL"),
|
||||||
|
base_url=os.getenv("OLLAMA_HOST"),
|
||||||
|
)
|
||||||
|
return FAISS.load_local(
|
||||||
|
"vector_store", embeddings, allow_dangerous_deserialization=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ollama_client():
|
||||||
|
return Client(
|
||||||
|
host=os.getenv("OLLAMA_HOST"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ollama_chat(client: Client, prompt: str):
|
||||||
|
db = load_vector_store()
|
||||||
|
|
||||||
|
# Get configurable parameters from environment
|
||||||
|
k_similar = int(
|
||||||
|
os.getenv("SIMILARITY_K", "5")
|
||||||
|
) # Number of similar chunks to retrieve
|
||||||
|
|
||||||
|
# Search for relevant context
|
||||||
|
relevant_chunks = db.similarity_search(prompt, k=k_similar)
|
||||||
|
|
||||||
|
context = "\n\n".join([doc.page_content for doc in relevant_chunks])
|
||||||
|
|
||||||
|
return client.chat(
|
||||||
|
model=os.getenv("OLLAMA_MODEL"),
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": """You are a command-line tool expert. Your role is to suggest the most appropriate command-line tools based on the user's needs.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. ONLY suggest tools that are explicitly mentioned in the provided context
|
||||||
|
2. Start with the most fundamental/common tool for the task
|
||||||
|
3. Format your response as:
|
||||||
|
Primary tool: [tool name] - [brief description]
|
||||||
|
Usage: [basic usage example]
|
||||||
|
|
||||||
|
Related tools:
|
||||||
|
- [alternative tool] - [why/when to use it]
|
||||||
|
|
||||||
|
4. If no suitable tool is found in the context, say "I don't see a suitable tool in my current knowledge base for that specific task"
|
||||||
|
5. Be concise and direct
|
||||||
|
6. Include specific, relevant details from the man pages
|
||||||
|
7. Don't make assumptions about tools that aren't in the context
|
||||||
|
|
||||||
|
Here is the relevant documentation context:
|
||||||
|
{}
|
||||||
|
""".format(
|
||||||
|
context
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
)
|
83
main.py
Normal file
83
main.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from libs.ollama_client import ollama_chat, ollama_client
|
||||||
|
from libs.get_all_mans import get_all_mans
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import sys
|
||||||
|
from colorama import init, Fore, Style
|
||||||
|
|
||||||
|
# Initialize colorama for cross-platform colored output
|
||||||
|
init()
|
||||||
|
|
||||||
|
|
||||||
|
def check_vector_store():
|
||||||
|
"""Check if vector store exists and is up to date"""
|
||||||
|
if not os.path.exists("vector_store"):
|
||||||
|
print(f"{Fore.YELLOW}Vector store not found{Style.RESET_ALL}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if any man page is newer than vector store
|
||||||
|
vector_store_time = datetime.fromtimestamp(
|
||||||
|
os.path.getmtime("vector_store"), timezone.utc
|
||||||
|
)
|
||||||
|
mans_dir = "mans"
|
||||||
|
|
||||||
|
if not os.path.exists(mans_dir):
|
||||||
|
print(f"{Fore.YELLOW}Man pages directory not found{Style.RESET_ALL}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
for man_file in os.listdir(mans_dir):
|
||||||
|
if man_file.endswith(".man"):
|
||||||
|
man_time = datetime.fromtimestamp(
|
||||||
|
os.path.getmtime(os.path.join(mans_dir, man_file)), timezone.utc
|
||||||
|
)
|
||||||
|
if man_time > vector_store_time:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Vector store outdated (newer man pages found){Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_knowledge_base():
|
||||||
|
"""Initialize or update the knowledge base with progress indicators"""
|
||||||
|
if check_vector_store():
|
||||||
|
print(f"{Fore.GREEN}✓ Vector store is up to date{Style.RESET_ALL}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"{Fore.CYAN}Initializing knowledge base...{Style.RESET_ALL}")
|
||||||
|
try:
|
||||||
|
db = get_all_mans()
|
||||||
|
print(f"{Fore.GREEN}✓ Successfully created vector store{Style.RESET_ALL}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Fore.RED}Error creating vector store: {e}{Style.RESET_ALL}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"{Fore.CYAN}Welcome to suggestoor!{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
# Initialize knowledge base
|
||||||
|
initialize_knowledge_base()
|
||||||
|
|
||||||
|
print(f"\n{Fore.GREEN}Ready to help! Type your questions below.{Style.RESET_ALL}")
|
||||||
|
print(f"{Fore.YELLOW}(Press Ctrl+C to exit){Style.RESET_ALL}\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
prompt = input(f"{Fore.CYAN}Which tool do you need? {Style.RESET_ALL}")
|
||||||
|
client = ollama_client()
|
||||||
|
print(f"{Fore.YELLOW}Thinking...{Style.RESET_ALL}")
|
||||||
|
response = ollama_chat(client, prompt)
|
||||||
|
print(f"\n{Fore.GREEN}Answer:{Style.RESET_ALL}")
|
||||||
|
print(response.message.content)
|
||||||
|
print() # Empty line for readability
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n{Fore.YELLOW}Goodbye!{Style.RESET_ALL}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Fore.RED}Error: {e}{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "suggestoor"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"colorama>=0.4.6",
|
||||||
|
"langchain-ollama>=0.2.2",
|
||||||
|
"langchain>=0.3.14",
|
||||||
|
"langchain-community>=0.3.14",
|
||||||
|
"ollama>=0.4.6",
|
||||||
|
"python-dotenv>=1.0.1",
|
||||||
|
"tqdm>=4.67.1",
|
||||||
|
"faiss-gpu-cu12>=1.9.0.post1",
|
||||||
|
]
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
colorama>=0.4.6
|
||||||
|
langchain-ollama>=0.2.2
|
||||||
|
langchain>=0.3.14
|
||||||
|
langchain-community>=0.3.14
|
||||||
|
ollama>=0.4.6
|
||||||
|
python-dotenv>=1.0.1
|
||||||
|
tqdm>=4.67.1
|
||||||
|
faiss-gpu-cu12>=1.9.0.post1
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 528 KiB |
Loading…
x
Reference in New Issue
Block a user