Compare commits
18 Commits
6526b38d4a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 10fc548326 | |||
| 9ea9f0bdf2 | |||
| 5a5ea3e390 | |||
| ea3bc792ff | |||
| b2888c08f4 | |||
| 3f94cb2eab | |||
| b8e90b3ef3 | |||
| 52679694f7 | |||
| ddd29b2581 | |||
| 13194bc74a | |||
| cefdaf3852 | |||
| 589b8acf8b | |||
| 411810a00c | |||
| ec8d17fa23 | |||
| bfbb91ec4f | |||
| d1e89420bf | |||
| 50e14ff1ac | |||
| 5b24a3d756 |
35
.gitea/workflows/ci.yml
Normal file
35
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout: 600
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r local-llm-prompt-manager/requirements.txt
|
||||
python -m pip install pytest pytest-cov ruff
|
||||
|
||||
- name: Run tests
|
||||
run: python -m pytest local-llm-prompt-manager/tests/ -v --tb=short
|
||||
|
||||
- name: Run linting
|
||||
run: python -m ruff check local-llm-prompt-manager/src/
|
||||
@@ -1,83 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout: 300
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ruff
|
||||
|
||||
- name: Run linting
|
||||
run: python -m ruff check repohealth-cli/src/ repohealth-cli/tests/
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout: 600
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r repohealth-cli/requirements.txt
|
||||
python -m pip install pytest pytest-cov
|
||||
|
||||
- name: Run tests
|
||||
run: python -m pytest repohealth-cli/tests/ -xvs --tb=short
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: .coverage
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout: 300
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r repohealth-cli/requirements.txt
|
||||
python -m pip install build
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
working-directory: ./repohealth-cli
|
||||
|
||||
98
.gitignore
vendored
98
.gitignore
vendored
@@ -1,98 +1,20 @@
|
||||
# =============================================================================
|
||||
# 7000%AUTO .gitignore
|
||||
# =============================================================================
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
.dist-info/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
.downloaded/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.venv/
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
|
||||
# Testing
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Database
|
||||
data/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Workspace (generated projects)
|
||||
workspace/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
.docker/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Secrets
|
||||
*.pem
|
||||
*.key
|
||||
secrets/
|
||||
|
||||
# Project specific
|
||||
*.readme_generated
|
||||
.readme_cache/
|
||||
|
||||
377
README.md
377
README.md
@@ -1,273 +1,210 @@
|
||||
# MCP Server CLI
|
||||
# Local LLM Prompt Manager
|
||||
|
||||
A CLI tool that creates a local Model Context Protocol (MCP) server for developers, enabling custom tool definitions in YAML/JSON with built-in file operations, git commands, shell execution, and local LLM support for offline AI coding assistant integration.
|
||||
A CLI tool for developers to manage, organize, version control, and share local LLM prompts and configurations.
|
||||
|
||||
## Features
|
||||
|
||||
- **Create & Manage Prompts**: Store prompts in YAML format with metadata
|
||||
- **Tagging System**: Organize prompts with tags for fast filtering
|
||||
- **Powerful Search**: Find prompts by name, content, or tags
|
||||
- **Template System**: Jinja2-style templating with variable substitution
|
||||
- **Git Integration**: Generate commit messages from staged changes
|
||||
- **LLM Integration**: Works with Ollama and LM Studio
|
||||
- **Import/Export**: Share prompts in JSON/YAML or LLM-specific formats
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# From source
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Or from source:
|
||||
|
||||
```bash
|
||||
git clone <repository>
|
||||
cd mcp-server-cli
|
||||
pip install -e .
|
||||
# Or install dependencies manually
|
||||
pip install click rich pyyaml requests jinja2
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Initialize a configuration file:
|
||||
### Create a Prompt
|
||||
|
||||
```bash
|
||||
mcp-server config init -o config.yaml
|
||||
llm-prompt prompt create my-prompt \
|
||||
--template "Explain {{ concept }} in simple terms" \
|
||||
--description "Simple explanation generator" \
|
||||
--tag "explanation" \
|
||||
--variable "concept:What to explain:true"
|
||||
```
|
||||
|
||||
2. Start the server:
|
||||
### List Prompts
|
||||
|
||||
```bash
|
||||
mcp-server server start --port 3000
|
||||
llm-prompt prompt list
|
||||
llm-prompt prompt list --tag "explanation"
|
||||
```
|
||||
|
||||
3. The server will be available at `http://127.0.0.1:3000`
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### Server Management
|
||||
### Run a Prompt with Variables
|
||||
|
||||
```bash
|
||||
# Start the MCP server
|
||||
mcp-server server start --port 3000 --host 127.0.0.1
|
||||
|
||||
# Check server status
|
||||
mcp-server server status
|
||||
|
||||
# Health check
|
||||
mcp-server health
|
||||
llm-prompt run my-prompt --var concept="quantum physics"
|
||||
```
|
||||
|
||||
### Tool Management
|
||||
### Generate a Commit Message
|
||||
|
||||
```bash
|
||||
# List available tools
|
||||
mcp-server tool list
|
||||
# Stage some changes first
|
||||
git add .
|
||||
|
||||
# Add a custom tool
|
||||
mcp-server tool add path/to/tool.yaml
|
||||
|
||||
# Remove a custom tool
|
||||
mcp-server tool remove tool_name
|
||||
# Generate commit message
|
||||
llm-prompt commit
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Prompt Management
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `llm-prompt prompt create <name>` | Create a new prompt |
|
||||
| `llm-prompt prompt list` | List all prompts |
|
||||
| `llm-prompt prompt show <name>` | Show prompt details |
|
||||
| `llm-prompt prompt delete <name>` | Delete a prompt |
|
||||
|
||||
### Tag Management
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `llm-prompt tag list` | List all tags |
|
||||
| `llm-prompt tag add <prompt> <tag>` | Add tag to prompt |
|
||||
| `llm-prompt tag remove <prompt> <tag>` | Remove tag from prompt |
|
||||
|
||||
### Search & Run
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `llm-prompt search [query]` | Search prompts |
|
||||
| `llm-prompt run <name>` | Run a prompt |
|
||||
|
||||
### Git Integration
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `llm-prompt commit` | Generate commit message |
|
||||
|
||||
### Import/Export
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `llm-prompt export <output>` | Export prompts |
|
||||
| `llm-prompt import <input>` | Import prompts |
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Show current configuration
|
||||
mcp-server config show
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `llm-prompt config show` | Show current config |
|
||||
| `llm-prompt config set <key> <value>` | Set config value |
|
||||
| `llm-prompt config test` | Test LLM connection |
|
||||
|
||||
# Generate a configuration file
|
||||
mcp-server config init -o config.yaml
|
||||
## Prompt Format
|
||||
|
||||
Prompts are stored as YAML files:
|
||||
|
||||
```yaml
|
||||
name: explain-code
|
||||
description: Explain code in simple terms
|
||||
tags: [documentation, beginner]
|
||||
variables:
|
||||
- name: code
|
||||
description: The code to explain
|
||||
required: true
|
||||
- name: level
|
||||
description: Explanation level (beginner/intermediate/advanced)
|
||||
required: false
|
||||
default: beginner
|
||||
template: |
|
||||
Explain the following code in {{ level }} terms:
|
||||
|
||||
```python
|
||||
{{ code }}
|
||||
```
|
||||
|
||||
provider: ollama
|
||||
model: llama3.2
|
||||
```
|
||||
|
||||
## Template System
|
||||
|
||||
Use Jinja2 syntax for variable substitution:
|
||||
|
||||
```jinja2
|
||||
Hello {{ name }}!
|
||||
Your task: {{ task }}
|
||||
|
||||
{% for item in items %}
|
||||
- {{ item }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Provide variables with `--var key=value`:
|
||||
|
||||
```bash
|
||||
llm-prompt run my-prompt --var name="Alice" --var task="review code"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `config.yaml` file:
|
||||
Default configuration file: `~/.config/llm-prompt-manager/config.yaml`
|
||||
|
||||
```yaml
|
||||
server:
|
||||
host: "127.0.0.1"
|
||||
port: 3000
|
||||
log_level: "INFO"
|
||||
|
||||
llm:
|
||||
enabled: false
|
||||
base_url: "http://localhost:11434"
|
||||
model: "llama2"
|
||||
|
||||
security:
|
||||
allowed_commands:
|
||||
- ls
|
||||
- cat
|
||||
- echo
|
||||
- git
|
||||
blocked_paths:
|
||||
- /etc
|
||||
- /root
|
||||
prompt_dir: ~/.config/llm-prompt-manager/prompts
|
||||
ollama_url: http://localhost:11434
|
||||
lmstudio_url: http://localhost:1234
|
||||
default_model: llama3.2
|
||||
default_provider: ollama
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MCP_PORT` | Server port |
|
||||
| `MCP_HOST` | Server host |
|
||||
| `MCP_LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) |
|
||||
| `MCP_LLM_URL` | Local LLM base URL |
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `LLM_PROMPT_DIR` | `~/.config/llm-prompt-manager/prompts` | Prompt storage directory |
|
||||
| `OLLAMA_URL` | `http://localhost:11434` | Ollama API URL |
|
||||
| `LMSTUDIO_URL` | `http://localhost:1234` | LM Studio API URL |
|
||||
| `DEFAULT_MODEL` | `llama3.2` | Default LLM model |
|
||||
|
||||
## Built-in Tools
|
||||
## Examples
|
||||
|
||||
### File Operations
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `file_tools` | Read, write, list, search, glob files |
|
||||
| `read_file` | Read file contents |
|
||||
| `write_file` | Write content to a file |
|
||||
| `list_directory` | List directory contents |
|
||||
| `glob_files` | Find files matching a pattern |
|
||||
|
||||
### Git Operations
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `git_tools` | Git operations: status, log, diff, branch |
|
||||
| `git_status` | Show working tree status |
|
||||
| `git_log` | Show commit history |
|
||||
| `git_diff` | Show changes between commits |
|
||||
|
||||
### Shell Execution
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `shell_tools` | Execute shell commands safely |
|
||||
| `execute_command` | Execute a shell command |
|
||||
|
||||
## Custom Tools
|
||||
|
||||
Define custom tools in YAML:
|
||||
|
||||
```yaml
|
||||
name: my_tool
|
||||
description: Description of the tool
|
||||
|
||||
input_schema:
|
||||
type: object
|
||||
properties:
|
||||
param1:
|
||||
type: string
|
||||
description: First parameter
|
||||
required: true
|
||||
param2:
|
||||
type: integer
|
||||
description: Second parameter
|
||||
default: 10
|
||||
required:
|
||||
- param1
|
||||
|
||||
annotations:
|
||||
read_only_hint: true
|
||||
destructive_hint: false
|
||||
```
|
||||
|
||||
Or in JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "example_tool",
|
||||
"description": "An example tool",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "The message to process",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Local LLM Integration
|
||||
|
||||
Connect to local LLMs (Ollama, LM Studio, llama.cpp):
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
enabled: true
|
||||
base_url: "http://localhost:11434"
|
||||
model: "llama2"
|
||||
temperature: 0.7
|
||||
max_tokens: 2048
|
||||
```
|
||||
|
||||
## Claude Desktop Integration
|
||||
|
||||
Add to `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-server": {
|
||||
"command": "mcp-server",
|
||||
"args": ["server", "start", "--port", "3000"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cursor Integration
|
||||
|
||||
Add to Cursor settings (JSON):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-server": {
|
||||
"command": "mcp-server",
|
||||
"args": ["server", "start", "--port", "3000"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Shell commands are whitelisted by default
|
||||
- Blocked paths prevent access to sensitive directories
|
||||
- Command timeout prevents infinite loops
|
||||
- All operations are logged
|
||||
|
||||
## API Reference
|
||||
|
||||
### Endpoints
|
||||
|
||||
- `GET /health` - Health check
|
||||
- `GET /api/tools` - List tools
|
||||
- `POST /api/tools/call` - Call a tool
|
||||
- `POST /mcp` - MCP protocol endpoint
|
||||
|
||||
### MCP Protocol
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocol_version": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"client_info": {"name": "client"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Read a File
|
||||
### Create a Code Explainer Prompt
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/tools/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "read_file", "arguments": {"path": "/path/to/file.txt"}}'
|
||||
llm-prompt prompt create explain-code \
|
||||
--template "Explain this code: {{ code }}" \
|
||||
--description "Explain code in simple terms" \
|
||||
--tag "docs" \
|
||||
--variable "code:The code to explain:true"
|
||||
```
|
||||
|
||||
### Example: List Tools
|
||||
### Export to Ollama Format
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/tools
|
||||
llm-prompt export my-prompts.yaml --format ollama
|
||||
```
|
||||
|
||||
### Import from Another Library
|
||||
|
||||
```bash
|
||||
llm-prompt import /path/to/prompts.json --force
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| Prompt not found | Use `llm-prompt list` to see available prompts |
|
||||
| LLM connection failed | Ensure Ollama/LM Studio is running |
|
||||
| Invalid YAML format | Check prompt YAML has required fields |
|
||||
| Template variable missing | Provide all required `--var` values |
|
||||
| Git repository not found | Run from a directory with `.git` folder |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
35
local-llm-prompt-manager/.gitea/workflows/ci.yml
Normal file
35
local-llm-prompt-manager/.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout: 600
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r local-llm-prompt-manager/requirements.txt
|
||||
python -m pip install pytest pytest-cov ruff
|
||||
|
||||
- name: Run tests
|
||||
run: python -m pytest local-llm-prompt-manager/tests/ -v --tb=short
|
||||
|
||||
- name: Run linting
|
||||
run: python -m ruff check local-llm-prompt-manager/src/
|
||||
@@ -3,71 +3,47 @@ requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "mcp-server-cli"
|
||||
version = "1.0.0"
|
||||
description = "A CLI tool that creates a local Model Context Protocol (MCP) server for developers"
|
||||
name = "local-llm-prompt-manager"
|
||||
version = "0.1.0"
|
||||
description = "A CLI tool for developers to manage, organize, version control, and share local LLM prompts and configurations"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.9"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "MCP Server CLI", email = "dev@example.com"}
|
||||
{name = "Developer", email = "dev@example.com"}
|
||||
]
|
||||
keywords = ["cli", "mcp", "model-context-protocol", "ai", "assistant"]
|
||||
keywords = ["llm", "prompt", "cli", "ollama", "lm-studio"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.12"
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"fastapi>=0.104.0",
|
||||
"click>=8.1.0",
|
||||
"pydantic>=2.5.0",
|
||||
"pyyaml>=6.0",
|
||||
"aiofiles>=23.2.0",
|
||||
"httpx>=0.25.0",
|
||||
"gitpython>=3.1.0",
|
||||
"uvicorn>=0.24.0",
|
||||
"sse-starlette>=1.6.0",
|
||||
"click>=8.1.7",
|
||||
"rich>=13.7.0",
|
||||
"pyyaml>=6.0.1",
|
||||
"requests>=2.31.0",
|
||||
"jinja2>=3.1.3"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"pytest-cov>=4.0",
|
||||
"ruff>=0.1.0",
|
||||
"pytest>=7.4.0",
|
||||
"pytest-cov>=4.1.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
llm-prompt = "src.cli:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
addopts = "-v --tb=short"
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/mcp_server_cli"]
|
||||
omit = ["*/tests/*", "*/__pycache__/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = ["pragma: no cover", "def __repr__", "raise NotImplementedError"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py38", "py39", "py310", "py311", "py312"]
|
||||
include = "\\.pyi?$"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I"]
|
||||
ignore = ["E501"]
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
fastapi==0.104.1
|
||||
click==8.1.7
|
||||
pydantic==2.5.0
|
||||
pyyaml==6.0.1
|
||||
aiofiles==23.2.1
|
||||
httpx==0.25.2
|
||||
gitpython==3.1.40
|
||||
uvicorn==0.24.0
|
||||
sse-starlette==1.6.5
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-cov==4.1.0
|
||||
click>=8.1.7
|
||||
rich>=13.7.0
|
||||
pyyaml>=6.0.1
|
||||
requests>=2.31.0
|
||||
jinja2>=3.1.3
|
||||
|
||||
# Development dependencies
|
||||
pytest>=7.4.0
|
||||
pytest-cov>=4.1.0
|
||||
|
||||
3
src/__init__.py
Normal file
3
src/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Local LLM Prompt Manager - A CLI tool for managing local LLM prompts."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
33
src/cli.py
Normal file
33
src/cli.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Main CLI entry point."""
|
||||
|
||||
import click
|
||||
|
||||
from .commands.commit import generate_commit
|
||||
from .commands.config import config_cmd
|
||||
from .commands.export import export_prompts
|
||||
from .commands.import_cmd import import_prompts
|
||||
from .commands.prompt import prompt_cmd
|
||||
from .commands.run import run_prompt, search_prompts
|
||||
from .commands.tag import tag_cmd
|
||||
|
||||
|
||||
@click.group()
|
||||
def main():
|
||||
"""Local LLM Prompt Manager - Manage, organize, and share your LLM prompts."""
|
||||
pass
|
||||
|
||||
|
||||
main.add_command(prompt_cmd, "prompt")
|
||||
main.add_command(prompt_cmd, "prompts")
|
||||
main.add_command(tag_cmd, "tag")
|
||||
main.add_command(tag_cmd, "tags")
|
||||
main.add_command(run_prompt, "run")
|
||||
main.add_command(search_prompts, "search")
|
||||
main.add_command(generate_commit, "commit")
|
||||
main.add_command(export_prompts, "export")
|
||||
main.add_command(import_prompts, "import")
|
||||
main.add_command(config_cmd, "config")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
123
src/models.py
Normal file
123
src/models.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Data models for the prompt manager."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromptVariable:
|
||||
"""Definition of a template variable."""
|
||||
name: str
|
||||
description: str = ""
|
||||
required: bool = True
|
||||
default: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Prompt:
|
||||
"""Represents a prompt template."""
|
||||
name: str
|
||||
template: str
|
||||
description: str = ""
|
||||
tags: list[str] = field(default_factory=list)
|
||||
variables: list[dict[str, Any]] = field(default_factory=list)
|
||||
provider: str = ""
|
||||
model: str = ""
|
||||
created_at: str = ""
|
||||
updated_at: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now().isoformat()
|
||||
if not self.updated_at:
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert prompt to dictionary."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"template": self.template,
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
"variables": self.variables,
|
||||
"provider": self.provider,
|
||||
"model": self.model,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Prompt":
|
||||
"""Create prompt from dictionary."""
|
||||
return cls(
|
||||
name=data.get("name", ""),
|
||||
template=data.get("template", ""),
|
||||
description=data.get("description", ""),
|
||||
tags=data.get("tags", []),
|
||||
variables=data.get("variables", []),
|
||||
provider=data.get("provider", ""),
|
||||
model=data.get("model", ""),
|
||||
created_at=data.get("created_at", ""),
|
||||
updated_at=data.get("updated_at", ""),
|
||||
)
|
||||
|
||||
def get_required_variables(self) -> list[str]:
|
||||
"""Get list of required variable names."""
|
||||
return [
|
||||
v["name"] for v in self.variables
|
||||
if v.get("required", True)
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tag:
|
||||
"""Represents a tag with associated prompts."""
|
||||
name: str
|
||||
prompts: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert tag to dictionary."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"prompts": self.prompts,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Tag":
|
||||
"""Create tag from dictionary."""
|
||||
return cls(
|
||||
name=data.get("name", ""),
|
||||
prompts=data.get("prompts", []),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Configuration settings."""
|
||||
prompt_dir: str = "~/.config/llm-prompt-manager/prompts"
|
||||
ollama_url: str = "http://localhost:11434"
|
||||
lmstudio_url: str = "http://localhost:1234"
|
||||
default_model: str = "llama3.2"
|
||||
default_provider: str = "ollama"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert config to dictionary."""
|
||||
return {
|
||||
"prompt_dir": self.prompt_dir,
|
||||
"ollama_url": self.ollama_url,
|
||||
"lmstudio_url": self.lmstudio_url,
|
||||
"default_model": self.default_model,
|
||||
"default_provider": self.default_provider,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Config":
|
||||
"""Create config from dictionary."""
|
||||
return cls(
|
||||
prompt_dir=data.get("prompt_dir", cls.prompt_dir),
|
||||
ollama_url=data.get("ollama_url", cls.ollama_url),
|
||||
lmstudio_url=data.get("lmstudio_url", cls.lmstudio_url),
|
||||
default_model=data.get("default_model", cls.default_model),
|
||||
default_provider=data.get("default_provider", cls.default_provider),
|
||||
)
|
||||
211
src/storage.py
Normal file
211
src/storage.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""Storage management for prompts and tags."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from .models import Config, Prompt, Tag
|
||||
|
||||
|
||||
class PromptStorage:
|
||||
"""Manages prompt storage in YAML files."""
|
||||
|
||||
def __init__(self, prompt_dir: str = None):
|
||||
if prompt_dir is None:
|
||||
config = ConfigManager().load()
|
||||
prompt_dir = config.prompt_dir
|
||||
self.prompt_dir = Path(prompt_dir)
|
||||
self._ensure_dir(str(self.prompt_dir))
|
||||
self.tags_index = self.prompt_dir / "tags.yaml"
|
||||
|
||||
def _ensure_dir(self, path: str) -> Path:
|
||||
"""Create directory if it doesn't exist."""
|
||||
path = Path(path)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
def get_prompt_path(self, name: str) -> Path:
|
||||
"""Get the file path for a prompt."""
|
||||
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name)
|
||||
return self.prompt_dir / f"{safe_name}.yaml"
|
||||
|
||||
def list_prompts(self) -> list[str]:
|
||||
"""List all prompt names."""
|
||||
prompts = []
|
||||
if self.prompt_dir.exists():
|
||||
for f in self.prompt_dir.glob("*.yaml"):
|
||||
if f.name != "tags.yaml":
|
||||
prompts.append(f.stem)
|
||||
return sorted(prompts)
|
||||
|
||||
def get_prompt(self, name: str) -> Prompt | None:
|
||||
"""Load a prompt by name."""
|
||||
path = self.get_prompt_path(name)
|
||||
if path.exists():
|
||||
with open(path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
if data:
|
||||
return Prompt.from_dict(data)
|
||||
return None
|
||||
|
||||
def save_prompt(self, prompt: Prompt) -> None:
|
||||
"""Save a prompt to file."""
|
||||
path = self.get_prompt_path(prompt.name)
|
||||
from datetime import datetime
|
||||
if not prompt.created_at:
|
||||
prompt.created_at = datetime.now().isoformat()
|
||||
prompt.updated_at = datetime.now().isoformat()
|
||||
with open(path, "w") as f:
|
||||
yaml.dump(prompt.to_dict(), f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
def delete_prompt(self, name: str) -> bool:
|
||||
"""Delete a prompt file."""
|
||||
path = self.get_prompt_path(name)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
self._remove_from_tags(name)
|
||||
return True
|
||||
return False
|
||||
|
||||
def prompt_exists(self, name: str) -> bool:
|
||||
"""Check if a prompt exists."""
|
||||
return self.get_prompt_path(name).exists()
|
||||
|
||||
def list_tags(self) -> list[str]:
|
||||
"""List all tags."""
|
||||
if self.tags_index.exists():
|
||||
with open(self.tags_index) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return list(data.keys())
|
||||
return []
|
||||
|
||||
def get_tag(self, name: str) -> Tag | None:
|
||||
"""Get a tag with its associated prompts."""
|
||||
if self.tags_index.exists():
|
||||
with open(self.tags_index) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
if name in data:
|
||||
return Tag(name=name, prompts=data[name])
|
||||
return None
|
||||
|
||||
def add_tag_to_prompt(self, prompt_name: str, tag: str) -> None:
|
||||
"""Add a tag to a prompt in the index."""
|
||||
data = {}
|
||||
if self.tags_index.exists():
|
||||
with open(self.tags_index) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
if tag not in data:
|
||||
data[tag] = []
|
||||
if prompt_name not in data[tag]:
|
||||
data[tag].append(prompt_name)
|
||||
with open(self.tags_index, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
def remove_tag_from_prompt(self, prompt_name: str, tag: str) -> bool:
|
||||
"""Remove a tag from a prompt in the index."""
|
||||
if not self.tags_index.exists():
|
||||
return False
|
||||
with open(self.tags_index) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
if tag in data and prompt_name in data[tag]:
|
||||
data[tag].remove(prompt_name)
|
||||
if not data[tag]:
|
||||
del data[tag]
|
||||
with open(self.tags_index, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _remove_from_tags(self, prompt_name: str) -> None:
|
||||
"""Remove prompt from all tags."""
|
||||
if not self.tags_index.exists():
|
||||
return
|
||||
with open(self.tags_index) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
modified = False
|
||||
for tag in list(data.keys()):
|
||||
if prompt_name in data[tag]:
|
||||
data[tag].remove(prompt_name)
|
||||
modified = True
|
||||
if not data[tag]:
|
||||
del data[tag]
|
||||
if modified:
|
||||
with open(self.tags_index, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
def search_prompts(
|
||||
self,
|
||||
name: str = None,
|
||||
content: str = None,
|
||||
tag: str = None
|
||||
) -> list[Prompt]:
|
||||
"""Search prompts by name, content, or tag."""
|
||||
results = []
|
||||
for prompt_name in self.list_prompts():
|
||||
prompt = self.get_prompt(prompt_name)
|
||||
if not prompt:
|
||||
continue
|
||||
if name and name.lower() not in prompt.name.lower():
|
||||
continue
|
||||
if content and content.lower() not in prompt.template.lower():
|
||||
continue
|
||||
if tag and tag.lower() not in [t.lower() for t in prompt.tags]:
|
||||
continue
|
||||
results.append(prompt)
|
||||
return results
|
||||
|
||||
def get_prompts_by_tag(self, tag: str) -> list[Prompt]:
|
||||
"""Get all prompts with a specific tag."""
|
||||
tag_data = self.get_tag(tag)
|
||||
if not tag_data:
|
||||
return []
|
||||
prompts = []
|
||||
for prompt_name in tag_data.prompts:
|
||||
prompt = self.get_prompt(prompt_name)
|
||||
if prompt:
|
||||
prompts.append(prompt)
|
||||
return prompts
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""Manages user configuration."""
|
||||
|
||||
def __init__(self, config_path: str = None):
|
||||
if config_path is None:
|
||||
home = Path.home()
|
||||
self.config_path = home / ".config" / "llm-prompt-manager" / "config.yaml"
|
||||
else:
|
||||
self.config_path = Path(config_path)
|
||||
|
||||
def _ensure_dir(self, path: str) -> Path:
|
||||
"""Create directory if it doesn't exist."""
|
||||
path = Path(path)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
def load(self) -> Config:
|
||||
"""Load configuration from file."""
|
||||
self._ensure_dir(str(self.config_path.parent))
|
||||
if self.config_path.exists():
|
||||
with open(self.config_path) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return Config.from_dict(data)
|
||||
return Config()
|
||||
|
||||
def save(self, config: Config) -> None:
|
||||
"""Save configuration to file."""
|
||||
self._ensure_dir(str(self.config_path.parent))
|
||||
with open(self.config_path, "w") as f:
|
||||
yaml.dump(config.to_dict(), f, default_flow_style=False)
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
"""Get a configuration value."""
|
||||
config = self.load()
|
||||
return getattr(config, key, None)
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""Set a configuration value."""
|
||||
config = self.load()
|
||||
setattr(config, key, value)
|
||||
self.save(config)
|
||||
Reference in New Issue
Block a user