Thank you for your interest in contributing to Inkwell! Whether you're fixing a bug, adding a feature, improving docs, or suggesting ideas — every contribution helps entrepreneurs find better conversations.
The live list lives on GitHub — good first issues. Filter by category:
- scanner — new platform integration (HN, Product Hunt, Dev.to, Lemmy…)
- exporter — new output target (Notion, Airtable, Slack…)
- persona — contribute a persona YAML to the marketplace
- help wanted — bigger features
Comment on an issue to claim it. Maintainer will assign.
Bigger roadmap bets that would love a second set of hands:
- Auto-persona from post history — Paste a Reddit/HN username → Inkwell fits a persona from the last 200 comments. Killer feature. (#15)
- Streaming drafts — Token-by-token via LiteLLM streaming. (#14)
- Daily email digest — APScheduler cron → top-N signals to your inbox each morning. (#13)
- Feedback loop — 1–5 star ratings feed back into
ai_preferencesweighting - Plugin system — Entry-point based discovery for community scanners/exporters
See the full direction in docs/ROADMAP.md.
Open an issue with:
- What you expected to happen
- What actually happened
- Steps to reproduce
- Your Python version and OS
- Any relevant log output (run with
-vfor verbose logging)
Open an issue with the feature label. Describe:
- The problem you're trying to solve
- Your proposed solution
- Any alternatives you considered
- Python 3.11 or newer
- Git
# 1. Fork the repo on GitHub, then clone your fork
git clone https://github.com/YOUR_USERNAME/inkwell.git
cd inkwell
# 2. Create a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# 3. Install in development mode with dev dependencies
pip install -e ".[dev]"
# 4. Copy and configure environment
cp .env.example .env
# Edit .env with your API keys (needed for integration tests)
# 5. Verify everything works
python -m inkwell --help
python -c "from inkwell.config import load_subreddits; print(f'OK: {len(load_subreddits())} subreddits')"# Run all tests
pytest
# Run with verbose output
pytest -v
# Run a specific test file
pytest tests/test_filters/test_rule_filter.py
# Run tests matching a keyword
pytest -k "reddit"# Lint with ruff
ruff check inkwell/
# Auto-fix lint issues
ruff check --fix inkwell/
# Format code
ruff format inkwell/inkwell/
├── inkwell/ # Main package
│ ├── scanners/ # Platform scanners (Reddit, HN, etc.)
│ ├── analyzers/ # AI analysis engine
│ ├── filters/ # Signal filtering
│ ├── personas/ # Voice/tone engine
│ ├── exporters/ # Output adapters (Sheets, CSV, etc.)
│ ├── storage/ # Local file storage (JSON)
│ ├── routes/ # FastAPI route handlers (Phase 1)
│ ├── templates/ # Jinja2 HTML templates (Phase 1)
│ ├── scheduler/ # Scan scheduling (Phase 1)
│ ├── config.py # Settings loader
│ ├── app.py # FastAPI app factory
│ └── __main__.py # CLI entry point
├── config/ # YAML configuration files
├── data/ # Runtime data (JSON files)
├── tests/ # Test suite
└── docs/ # Documentation
| Module | What to know |
|---|---|
scanners/ |
Each scanner implements the Scanner protocol. Add new platforms here. |
analyzers/ |
AI pipeline: prompt building → LLM call → JSON parsing. Edit prompts in pipeline.py. |
filters/ |
Rule-based filtering runs BEFORE AI. Zero cost. Add new filter types here. |
personas/ |
Builds prompt blocks from personality.yml. Touch this to change how personality is injected. |
exporters/ |
Each exporter implements the Exporter protocol. Add new outputs here. |
storage/ |
All JSON file CRUD. Each data type has its own module. |
config.py |
Central config. Everything imports from here. |
__main__.py |
CLI orchestration. The scan loop lives here. |
This is one of the most impactful contributions. Here's a step-by-step guide:
# inkwell/scanners/hackernews.py
import logging
import time
from inkwell.scanners.base import RawSignal, Reply, fetch_json
from inkwell.scanners import registry
logger = logging.getLogger(__name__)
HN_API = "https://hn.algolia.com/api/v1"
class HackerNewsScanner:
name = "hackernews"
def scan(self, targets: list[str], max_age_hours: int = 24) -> list[RawSignal]:
"""Scan HN for recent stories. Targets = search queries or 'front_page'."""
signals = []
# Your implementation here...
# Use fetch_json() from base.py for HTTP with retries
# Return list of RawSignal objects
return signals
registry.register(HackerNewsScanner())Edit inkwell/scanners/registry.py:
def _ensure_loaded():
global _loaded
if _loaded:
return
_loaded = True
from inkwell.scanners import reddit # noqa: F401
from inkwell.scanners import hackernews # noqa: F401 <-- add thisEdit inkwell/__main__.py to add a --hackernews flag and call the scanner.
# tests/test_scanners/test_hackernews.py
from inkwell.scanners.hackernews import HackerNewsScanner
def test_hackernews_scanner_has_name():
scanner = HackerNewsScanner()
assert scanner.name == "hackernews"
# Add more tests with mocked HTTP responses# config/hackernews.yml
queries:
- "Show HN"
- "Ask HN"
min_points: 5# inkwell/exporters/slack_webhook.py
import logging
import requests
logger = logging.getLogger(__name__)
class SlackWebhookExporter:
name = "slack"
def export(self, rows: list[dict], config: dict | None = None) -> None:
webhook_url = (config or {}).get("webhook_url")
if not webhook_url:
logger.error("No webhook_url provided for Slack export")
return
for row in rows:
if row.get("Engage?") != "Yes":
continue
text = f"*{row.get('Post title', '')}*\n{row.get('Summary', '')}\n<{row.get('Post link', '')}>"
requests.post(webhook_url, json={"text": text})
logger.info("Posted signal to Slack: %s", row.get("Post title", "")[:50])- Python 3.11+ — Use modern syntax (type hints,
match,|union types) - Line length — 100 characters max (configured in
pyproject.toml) - Formatting — Run
ruff formatbefore committing - Linting — Run
ruff checkand fix all issues - Type hints — Add type hints to function signatures. No need for inline variable types unless ambiguous.
- Logging — Use
logging.getLogger(__name__)instead ofprint(). Use appropriate levels:logger.debug()— detailed diagnostic infologger.info()— progress updates, key milestoneslogger.warning()— recoverable issueslogger.error()— failures that affect output
- Docstrings — One-line docstrings for simple functions. Multi-line for complex ones. Don't add docstrings to trivially obvious functions.
- No unnecessary comments — Code should be self-documenting. Only comment on the "why", not the "what".
- Files:
snake_case.py - Classes:
PascalCase - Functions/methods:
snake_case - Constants:
UPPER_SNAKE_CASE - Private/internal: prefix with
_
- Standard library
- Third-party packages
- Local imports (
from inkwell...)
Separated by blank lines.
-
Create a branch from
main:git checkout -b feature/hackernews-scanner
-
Make your changes — keep PRs focused on one thing
-
Test your changes:
pytest ruff check inkwell/
-
Verify the CLI still works:
python -m inkwell --help python -m inkwell scan --no-sheets # quick test (if you have API keys)
- Keep PRs small and focused. One feature, one bug fix, or one improvement per PR.
- Write a clear description. What does this change? Why? How can it be tested?
- Add tests for new functionality.
- Update docs if your change affects user-facing behavior.
- Don't refactor unrelated code in the same PR.
## What
Brief description of the change.
## Why
The problem or need this addresses.
## How to Test
Steps to verify the change works:
1. ...
2. ...
## Checklist
- [ ] Tests pass (`pytest`)
- [ ] Lint passes (`ruff check`)
- [ ] Docs updated (if applicable)
- [ ] No unrelated changes included- PRs are reviewed within 48 hours (we aim for 24h)
- Feedback is about code quality, not personal preference
- Small nits are fixed by the maintainer when merging, not sent back for revision
- Approved PRs are squash-merged to keep
mainhistory clean
| Label | Description |
|---|---|
good first issue |
Great for newcomers |
help wanted |
We'd love a contribution here |
bug |
Something isn't working |
feature |
New functionality |
scanner |
New platform scanner |
exporter |
New output adapter |
ui |
Web dashboard related |
docs |
Documentation improvement |
performance |
Speed or resource optimization |
- GitHub Issues — Bug reports, feature requests, and discussions
- Pull Requests — Code contributions
- Discussions — Open-ended questions and ideas (when enabled)
Be kind, be constructive, be helpful. We're building this for entrepreneurs who are trying to make something meaningful. Treat other contributors the way you'd want to be treated.
Specifically:
- Welcome newcomers — everyone starts somewhere
- Give constructive feedback — explain the "why" behind suggestions
- Assume good intent — text is easily misread
- Focus on the code, not the person
- No harassment, discrimination, or personal attacks
All contributors are recognized in the project:
- Significant contributions are highlighted in release notes
- Regular contributors may be invited as project maintainers
Open an issue with the question label, or start a discussion. We're happy to help you get started with your first contribution.
Thank you for helping make Inkwell better for every entrepreneur out there.