Compare commits

..

18 Commits

Author SHA1 Message Date
10fc548326 fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Failing after 4m43s
2026-02-05 23:04:09 +00:00
9ea9f0bdf2 fix: Fix CI paths for requirements and tests
Some checks failed
CI / test (push) Failing after 4m45s
2026-02-05 22:55:12 +00:00
5a5ea3e390 fix: Scope CI linting to project directory only
Some checks failed
CI / test (push) Failing after 4m46s
2026-02-05 22:47:25 +00:00
ea3bc792ff fix: Remove unnecessary cd command - CI runs from repo root
Some checks failed
CI / test (push) Failing after 4m45s
2026-02-05 22:39:44 +00:00
b2888c08f4 fix: Update CI workflow to run from project subdirectory
Some checks failed
CI / test (push) Failing after 4m47s
2026-02-05 22:33:09 +00:00
3f94cb2eab fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Failing after 4m46s
2026-02-05 22:25:36 +00:00
b8e90b3ef3 fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:36 +00:00
52679694f7 fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:35 +00:00
ddd29b2581 fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:33 +00:00
13194bc74a fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:32 +00:00
cefdaf3852 fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:31 +00:00
589b8acf8b fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:31 +00:00
411810a00c fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:30 +00:00
ec8d17fa23 fix: Correct CI workflow directory paths
Some checks failed
CI / test (push) Has been cancelled
2026-02-05 22:25:30 +00:00
bfbb91ec4f Add Gitea Actions workflow: ci.yml
Some checks failed
CI / lint (push) Failing after 4m44s
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-05 22:20:36 +00:00
d1e89420bf chore: remove old repohealth.yml workflow file
Some checks failed
CI / lint (push) Successful in 9m25s
CI / test (push) Failing after 4m45s
CI / build (push) Has been skipped
2026-02-05 21:50:25 +00:00
50e14ff1ac fix: rename workflow to ci.yml for Gitea Actions
Some checks failed
CI / lint (push) Successful in 9m26s
CI / test (push) Failing after 4m46s
CI / build (push) Has been skipped
2026-02-05 21:49:56 +00:00
5b24a3d756 fix: correct working-directory in CI workflow
Some checks failed
CI / lint (push) Successful in 9m25s
CI / test (push) Failing after 4m46s
CI / build (push) Has been skipped
2026-02-05 21:34:11 +00:00
11 changed files with 636 additions and 447 deletions

35
.gitea/workflows/ci.yml Normal file
View 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/

View File

@@ -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
View File

@@ -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
View File

@@ -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

View 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/

View File

@@ -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"]

View File

@@ -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
View 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
View 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
View 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
View 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)