feat: Support installing built-in AGENT files and skills during picoclaw onboard
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ build/
|
|||||||
*.out
|
*.out
|
||||||
/picoclaw
|
/picoclaw
|
||||||
/picoclaw-test
|
/picoclaw-test
|
||||||
|
cmd/picoclaw/workspace
|
||||||
|
|
||||||
# Picoclaw specific
|
# Picoclaw specific
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,8 @@ RUN apk add --no-cache ca-certificates tzdata
|
|||||||
# Copy binary
|
# Copy binary
|
||||||
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
||||||
|
|
||||||
# Copy builtin skills
|
|
||||||
COPY --from=builder /src/skills /opt/picoclaw/skills
|
|
||||||
|
|
||||||
# Create picoclaw home directory
|
# Create picoclaw home directory
|
||||||
RUN mkdir -p /root/.picoclaw/workspace/skills && \
|
RUN /usr/local/bin/picoclaw onboard
|
||||||
cp -r /opt/picoclaw/skills/* /root/.picoclaw/workspace/skills/ 2>/dev/null || true
|
|
||||||
|
|
||||||
ENTRYPOINT ["picoclaw"]
|
ENTRYPOINT ["picoclaw"]
|
||||||
CMD ["gateway"]
|
CMD ["gateway"]
|
||||||
|
|||||||
31
Makefile
31
Makefile
@@ -67,6 +67,8 @@ all: build
|
|||||||
build:
|
build:
|
||||||
@echo "Building $(BINARY_NAME) for $(PLATFORM)/$(ARCH)..."
|
@echo "Building $(BINARY_NAME) for $(PLATFORM)/$(ARCH)..."
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true
|
||||||
|
@cp -r workspace ./$(CMD_DIR)/workspace 2>/dev/null || true
|
||||||
$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BINARY_PATH) ./$(CMD_DIR)
|
$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BINARY_PATH) ./$(CMD_DIR)
|
||||||
@echo "Build complete: $(BINARY_PATH)"
|
@echo "Build complete: $(BINARY_PATH)"
|
||||||
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
|
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
|
||||||
@@ -74,6 +76,8 @@ build:
|
|||||||
## build-all: Build picoclaw for all platforms
|
## build-all: Build picoclaw for all platforms
|
||||||
build-all:
|
build-all:
|
||||||
@echo "Building for multiple platforms..."
|
@echo "Building for multiple platforms..."
|
||||||
|
@rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true
|
||||||
|
@cp -r workspace ./$(CMD_DIR)/workspace 2>/dev/null || true
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
|
GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
|
||||||
GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
|
GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
|
||||||
@@ -89,35 +93,8 @@ install: build
|
|||||||
@cp $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_BIN_DIR)/$(BINARY_NAME)
|
@cp $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_BIN_DIR)/$(BINARY_NAME)
|
||||||
@chmod +x $(INSTALL_BIN_DIR)/$(BINARY_NAME)
|
@chmod +x $(INSTALL_BIN_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installed binary to $(INSTALL_BIN_DIR)/$(BINARY_NAME)"
|
@echo "Installed binary to $(INSTALL_BIN_DIR)/$(BINARY_NAME)"
|
||||||
@echo "Installing builtin skills to $(WORKSPACE_SKILLS_DIR)..."
|
|
||||||
@mkdir -p $(WORKSPACE_SKILLS_DIR)
|
|
||||||
@for skill in $(BUILTIN_SKILLS_DIR)/*/; do \
|
|
||||||
if [ -d "$$skill" ]; then \
|
|
||||||
skill_name=$$(basename "$$skill"); \
|
|
||||||
if [ -f "$$skill/SKILL.md" ]; then \
|
|
||||||
cp -r "$$skill" $(WORKSPACE_SKILLS_DIR); \
|
|
||||||
echo " ✓ Installed skill: $$skill_name"; \
|
|
||||||
fi; \
|
|
||||||
fi; \
|
|
||||||
done
|
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
|
|
||||||
## install-skills: Install builtin skills to workspace
|
|
||||||
install-skills:
|
|
||||||
@echo "Installing builtin skills to $(WORKSPACE_SKILLS_DIR)..."
|
|
||||||
@mkdir -p $(WORKSPACE_SKILLS_DIR)
|
|
||||||
@for skill in $(BUILTIN_SKILLS_DIR)/*/; do \
|
|
||||||
if [ -d "$$skill" ]; then \
|
|
||||||
skill_name=$$(basename "$$skill"); \
|
|
||||||
if [ -f "$$skill/SKILL.md" ]; then \
|
|
||||||
mkdir -p $(WORKSPACE_SKILLS_DIR)/$$skill_name; \
|
|
||||||
cp -r "$$skill" $(WORKSPACE_SKILLS_DIR); \
|
|
||||||
echo " ✓ Installed skill: $$skill_name"; \
|
|
||||||
fi; \
|
|
||||||
fi; \
|
|
||||||
done
|
|
||||||
@echo "Skills installation complete!"
|
|
||||||
|
|
||||||
## uninstall: Remove picoclaw from system
|
## uninstall: Remove picoclaw from system
|
||||||
uninstall:
|
uninstall:
|
||||||
@echo "Uninstalling $(BINARY_NAME)..."
|
@echo "Uninstalling $(BINARY_NAME)..."
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"embed"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
"github.com/sipeed/picoclaw/pkg/agent"
|
"github.com/sipeed/picoclaw/pkg/agent"
|
||||||
@@ -36,6 +38,10 @@ import (
|
|||||||
"github.com/sipeed/picoclaw/pkg/voice"
|
"github.com/sipeed/picoclaw/pkg/voice"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed workspace
|
||||||
|
var embeddedFiles embed.FS
|
||||||
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "dev"
|
version = "dev"
|
||||||
gitCommit string
|
gitCommit string
|
||||||
@@ -208,6 +214,7 @@ func printHelp() {
|
|||||||
fmt.Println(" version Show version information")
|
fmt.Println(" version Show version information")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func onboard() {
|
func onboard() {
|
||||||
configPath := getConfigPath()
|
configPath := getConfigPath()
|
||||||
|
|
||||||
@@ -229,10 +236,6 @@ func onboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspace := cfg.WorkspacePath()
|
workspace := cfg.WorkspacePath()
|
||||||
os.MkdirAll(workspace, 0755)
|
|
||||||
os.MkdirAll(filepath.Join(workspace, "memory"), 0755)
|
|
||||||
os.MkdirAll(filepath.Join(workspace, "skills"), 0755)
|
|
||||||
|
|
||||||
createWorkspaceTemplates(workspace)
|
createWorkspaceTemplates(workspace)
|
||||||
|
|
||||||
fmt.Printf("%s picoclaw is ready!\n", logo)
|
fmt.Printf("%s picoclaw is ready!\n", logo)
|
||||||
@@ -242,170 +245,57 @@ func onboard() {
|
|||||||
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyEmbeddedToTarget(targetDir string) error {
|
||||||
|
// Ensure target directory exists
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("Failed to create target directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk through all files in embed.FS
|
||||||
|
err := fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip directories
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read embedded file
|
||||||
|
data, err := embeddedFiles.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to read embedded file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
new_path, err := filepath.Rel("workspace", path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build target file path
|
||||||
|
targetPath := filepath.Join(targetDir, new_path)
|
||||||
|
|
||||||
|
// Ensure target file's directory exists
|
||||||
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file
|
||||||
|
if err := os.WriteFile(targetPath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func createWorkspaceTemplates(workspace string) {
|
func createWorkspaceTemplates(workspace string) {
|
||||||
templates := map[string]string{
|
err := copyEmbeddedToTarget(workspace)
|
||||||
"AGENTS.md": `# Agent Instructions
|
if err != nil {
|
||||||
|
fmt.Printf("Error copying workspace templates: %v\n", err)
|
||||||
You are a helpful AI assistant. Be concise, accurate, and friendly.
|
|
||||||
|
|
||||||
## Guidelines
|
|
||||||
|
|
||||||
- Always explain what you're doing before taking actions
|
|
||||||
- Ask for clarification when request is ambiguous
|
|
||||||
- Use tools to help accomplish tasks
|
|
||||||
- Remember important information in your memory files
|
|
||||||
- Be proactive and helpful
|
|
||||||
- Learn from user feedback
|
|
||||||
`,
|
|
||||||
"SOUL.md": `# Soul
|
|
||||||
|
|
||||||
I am picoclaw, a lightweight AI assistant powered by AI.
|
|
||||||
|
|
||||||
## Personality
|
|
||||||
|
|
||||||
- Helpful and friendly
|
|
||||||
- Concise and to the point
|
|
||||||
- Curious and eager to learn
|
|
||||||
- Honest and transparent
|
|
||||||
|
|
||||||
## Values
|
|
||||||
|
|
||||||
- Accuracy over speed
|
|
||||||
- User privacy and safety
|
|
||||||
- Transparency in actions
|
|
||||||
- Continuous improvement
|
|
||||||
`,
|
|
||||||
"USER.md": `# User
|
|
||||||
|
|
||||||
Information about user goes here.
|
|
||||||
|
|
||||||
## Preferences
|
|
||||||
|
|
||||||
- Communication style: (casual/formal)
|
|
||||||
- Timezone: (your timezone)
|
|
||||||
- Language: (your preferred language)
|
|
||||||
|
|
||||||
## Personal Information
|
|
||||||
|
|
||||||
- Name: (optional)
|
|
||||||
- Location: (optional)
|
|
||||||
- Occupation: (optional)
|
|
||||||
|
|
||||||
## Learning Goals
|
|
||||||
|
|
||||||
- What the user wants to learn from AI
|
|
||||||
- Preferred interaction style
|
|
||||||
- Areas of interest
|
|
||||||
`,
|
|
||||||
"IDENTITY.md": `# Identity
|
|
||||||
|
|
||||||
## Name
|
|
||||||
PicoClaw 🦞
|
|
||||||
|
|
||||||
## Description
|
|
||||||
Ultra-lightweight personal AI assistant written in Go, inspired by nanobot.
|
|
||||||
|
|
||||||
## Version
|
|
||||||
0.1.0
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
- Provide intelligent AI assistance with minimal resource usage
|
|
||||||
- Support multiple LLM providers (OpenAI, Anthropic, Zhipu, etc.)
|
|
||||||
- Enable easy customization through skills system
|
|
||||||
- Run on minimal hardware ($10 boards, <10MB RAM)
|
|
||||||
|
|
||||||
## Capabilities
|
|
||||||
|
|
||||||
- Web search and content fetching
|
|
||||||
- File system operations (read, write, edit)
|
|
||||||
- Shell command execution
|
|
||||||
- Multi-channel messaging (Telegram, WhatsApp, Feishu)
|
|
||||||
- Skill-based extensibility
|
|
||||||
- Memory and context management
|
|
||||||
|
|
||||||
## Philosophy
|
|
||||||
|
|
||||||
- Simplicity over complexity
|
|
||||||
- Performance over features
|
|
||||||
- User control and privacy
|
|
||||||
- Transparent operation
|
|
||||||
- Community-driven development
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Provide a fast, lightweight AI assistant
|
|
||||||
- Support offline-first operation where possible
|
|
||||||
- Enable easy customization and extension
|
|
||||||
- Maintain high quality responses
|
|
||||||
- Run efficiently on constrained hardware
|
|
||||||
|
|
||||||
## License
|
|
||||||
MIT License - Free and open source
|
|
||||||
|
|
||||||
## Repository
|
|
||||||
https://github.com/sipeed/picoclaw
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
Issues: https://github.com/sipeed/picoclaw/issues
|
|
||||||
Discussions: https://github.com/sipeed/picoclaw/discussions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
"Every bit helps, every bit matters."
|
|
||||||
- Picoclaw
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
|
|
||||||
for filename, content := range templates {
|
|
||||||
filePath := filepath.Join(workspace, filename)
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
||||||
os.WriteFile(filePath, []byte(content), 0644)
|
|
||||||
fmt.Printf(" Created %s\n", filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryDir := filepath.Join(workspace, "memory")
|
|
||||||
os.MkdirAll(memoryDir, 0755)
|
|
||||||
memoryFile := filepath.Join(memoryDir, "MEMORY.md")
|
|
||||||
if _, err := os.Stat(memoryFile); os.IsNotExist(err) {
|
|
||||||
memoryContent := `# Long-term Memory
|
|
||||||
|
|
||||||
This file stores important information that should persist across sessions.
|
|
||||||
|
|
||||||
## User Information
|
|
||||||
|
|
||||||
(Important facts about user)
|
|
||||||
|
|
||||||
## Preferences
|
|
||||||
|
|
||||||
(User preferences learned over time)
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
(Things to remember)
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
- Model preferences
|
|
||||||
- Channel settings
|
|
||||||
- Skills enabled
|
|
||||||
`
|
|
||||||
os.WriteFile(memoryFile, []byte(memoryContent), 0644)
|
|
||||||
fmt.Println(" Created memory/MEMORY.md")
|
|
||||||
|
|
||||||
skillsDir := filepath.Join(workspace, "skills")
|
|
||||||
if _, err := os.Stat(skillsDir); os.IsNotExist(err) {
|
|
||||||
os.MkdirAll(skillsDir, 0755)
|
|
||||||
fmt.Println(" Created skills/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for filename, content := range templates {
|
|
||||||
filePath := filepath.Join(workspace, filename)
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
||||||
os.WriteFile(filePath, []byte(content), 0644)
|
|
||||||
fmt.Printf(" Created %s\n", filename)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
workspace/AGENT.md
Normal file
12
workspace/AGENT.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
You are a helpful AI assistant. Be concise, accurate, and friendly.
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Always explain what you're doing before taking actions
|
||||||
|
- Ask for clarification when request is ambiguous
|
||||||
|
- Use tools to help accomplish tasks
|
||||||
|
- Remember important information in your memory files
|
||||||
|
- Be proactive and helpful
|
||||||
|
- Learn from user feedback
|
||||||
56
workspace/IDENTITY.md
Normal file
56
workspace/IDENTITY.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Identity
|
||||||
|
|
||||||
|
## Name
|
||||||
|
PicoClaw 🦞
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Ultra-lightweight personal AI assistant written in Go, inspired by nanobot.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
0.1.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
- Provide intelligent AI assistance with minimal resource usage
|
||||||
|
- Support multiple LLM providers (OpenAI, Anthropic, Zhipu, etc.)
|
||||||
|
- Enable easy customization through skills system
|
||||||
|
- Run on minimal hardware ($10 boards, <10MB RAM)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Web search and content fetching
|
||||||
|
- File system operations (read, write, edit)
|
||||||
|
- Shell command execution
|
||||||
|
- Multi-channel messaging (Telegram, WhatsApp, Feishu)
|
||||||
|
- Skill-based extensibility
|
||||||
|
- Memory and context management
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
- Simplicity over complexity
|
||||||
|
- Performance over features
|
||||||
|
- User control and privacy
|
||||||
|
- Transparent operation
|
||||||
|
- Community-driven development
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Provide a fast, lightweight AI assistant
|
||||||
|
- Support offline-first operation where possible
|
||||||
|
- Enable easy customization and extension
|
||||||
|
- Maintain high quality responses
|
||||||
|
- Run efficiently on constrained hardware
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT License - Free and open source
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
https://github.com/sipeed/picoclaw
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
Issues: https://github.com/sipeed/picoclaw/issues
|
||||||
|
Discussions: https://github.com/sipeed/picoclaw/discussions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
"Every bit helps, every bit matters."
|
||||||
|
- Picoclaw
|
||||||
17
workspace/SOUL.md
Normal file
17
workspace/SOUL.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Soul
|
||||||
|
|
||||||
|
I am picoclaw, a lightweight AI assistant powered by AI.
|
||||||
|
|
||||||
|
## Personality
|
||||||
|
|
||||||
|
- Helpful and friendly
|
||||||
|
- Concise and to the point
|
||||||
|
- Curious and eager to learn
|
||||||
|
- Honest and transparent
|
||||||
|
|
||||||
|
## Values
|
||||||
|
|
||||||
|
- Accuracy over speed
|
||||||
|
- User privacy and safety
|
||||||
|
- Transparency in actions
|
||||||
|
- Continuous improvement
|
||||||
21
workspace/USER.md
Normal file
21
workspace/USER.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# User
|
||||||
|
|
||||||
|
Information about user goes here.
|
||||||
|
|
||||||
|
## Preferences
|
||||||
|
|
||||||
|
- Communication style: (casual/formal)
|
||||||
|
- Timezone: (your timezone)
|
||||||
|
- Language: (your preferred language)
|
||||||
|
|
||||||
|
## Personal Information
|
||||||
|
|
||||||
|
- Name: (optional)
|
||||||
|
- Location: (optional)
|
||||||
|
- Occupation: (optional)
|
||||||
|
|
||||||
|
## Learning Goals
|
||||||
|
|
||||||
|
- What the user wants to learn from AI
|
||||||
|
- Preferred interaction style
|
||||||
|
- Areas of interest
|
||||||
21
workspace/memory/MEMORY.md
Normal file
21
workspace/memory/MEMORY.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Long-term Memory
|
||||||
|
|
||||||
|
This file stores important information that should persist across sessions.
|
||||||
|
|
||||||
|
## User Information
|
||||||
|
|
||||||
|
(Important facts about user)
|
||||||
|
|
||||||
|
## Preferences
|
||||||
|
|
||||||
|
(User preferences learned over time)
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
(Things to remember)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Model preferences
|
||||||
|
- Channel settings
|
||||||
|
- Skills enabled
|
||||||
Reference in New Issue
Block a user