diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a6b3102 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,42 @@ +name: Publish + +on: + push: + tags: ["v*"] + +permissions: + contents: write # GitHub release + id-token: write # PyPI trusted publishing + +jobs: + pypi: + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # setuptools-scm needs full history + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build + run: | + pip install build + python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + needs: pypi + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release create "${{ github.ref_name }}" --generate-notes + diff --git a/.gitignore b/.gitignore index 6295979..2c80c10 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ .entire/ __pycache__/ *.pyc +dist/ +*.egg-info/ +build/ bench/results/tmp-* diff --git a/CLAUDE.md b/CLAUDE.md index 6acfe9b..57eebdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,22 +8,24 @@ Repo-owned memory for AI coding agents. Reads raw evidence from Entire CLI sessi ## Structure -- `reflect` — CLI entry point (Python) -- `lib/` — CLI modules (evidence, context, init, why, search, status, improve) -- `lib/evidence.py` — fixed evidence gathering pipeline (Entire CLI + git) +- `reflect` — CLI entry point (legacy dev symlink, kept for backwards compat) +- `reflect_cli/` — Python package (evidence, context, init, why, search, status, improve) +- `reflect_cli/cli.py` — main entry point (used by pyproject.toml console_scripts) +- `reflect_cli/evidence.py` — fixed evidence gathering pipeline (Entire CLI + git) +- `pyproject.toml` — package config for pip/pipx distribution - `skill/SKILL.md` — skill source (dev copy; install copies to `.claude/skills/reflect/`) - `SPEC.md` — specification for `.reflect/` directory format - `hooks/session-start.sh` — SessionStart hook for context freshness (also linked from the skill dir) -- `install.sh` — installer (symlinks CLI to `~/.local/bin`) +- `install.sh` — curl-friendly installer (auto-detects pipx/pip/git) - `README.md` — user-facing docs - `ROADMAP.md` — future phases - `CLAUDE.md` — this file ## Development -- Edit `lib/evidence.py` to change evidence gathering -- Edit `lib/context.py` to change synthesis pipeline, system prompt, or validation -- Edit `lib/` to change CLI commands +- Edit `reflect_cli/evidence.py` to change evidence gathering +- Edit `reflect_cli/context.py` to change synthesis pipeline, system prompt, or validation +- Edit `reflect_cli/` to change CLI commands - Edit `.reflect/format.yaml` (in any repo) to customize context sections - Edit `skill/SKILL.md` to change the Claude Code skill (source of truth) - Test locally: `python3 reflect context` or `python3 reflect why ` diff --git a/README.md b/README.md index 7a814ca..5f285c4 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,15 @@ --- -## Quick Start +## Install ```bash -# Install reflect -git clone https://github.com/codeyogi911/reflect.git -cd reflect && ./install.sh +curl -fsSL https://raw.githubusercontent.com/codeyogi911/reflect/main/install.sh | bash +``` +## Quick Start + +```bash # Set up any repo cd ~/your-project reflect init # installs Entire CLI, creates .reflect/, wires into CLAUDE.md @@ -180,9 +182,10 @@ Claude's memory lives in `~/.claude/projects/` on your laptop — it doesn't tra ## Contributing 1. Fork the repo -2. Edit `lib/` — changes take effect immediately via symlinks -3. Test: `python3 reflect context` or `python3 reflect why ` -4. Submit a PR +2. `pip install -e .` for an editable install +3. Edit `reflect_cli/` — changes take effect immediately +4. Test: `reflect context` or `reflect why ` +5. Submit a PR ## License diff --git a/install.sh b/install.sh index 911ba36..acda8ec 100755 --- a/install.sh +++ b/install.sh @@ -1,35 +1,53 @@ #!/usr/bin/env bash +# reflect installer — curl -fsSL https://raw.githubusercontent.com/codeyogi911/reflect/main/install.sh | bash set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# ── CLI: symlink to ~/.local/bin ────────────────────────────────────── -BIN_DIR="${HOME}/.local/bin" -mkdir -p "$BIN_DIR" -ln -sf "$SCRIPT_DIR/reflect" "$BIN_DIR/reflect" -echo "CLI installed: $BIN_DIR/reflect" - -# ── Skill: install into target repo ────────────────────────────────── -# If run from within a git repo, install the skill there. -# Otherwise install into the reflect repo itself. -TARGET_REPO="${1:-$(git rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")}" - -SKILL_SRC="$SCRIPT_DIR/skill/SKILL.md" -SKILL_DST="$TARGET_REPO/.claude/skills/reflect" - -mkdir -p "$SKILL_DST" -cp "$SKILL_SRC" "$SKILL_DST/SKILL.md" - -# Copy hooks if they exist -HOOKS_DIR="$SCRIPT_DIR/hooks" -if [ -d "$HOOKS_DIR" ]; then - rm -rf "$SKILL_DST/hooks" - cp -R "$HOOKS_DIR" "$SKILL_DST/hooks" +REPO="codeyogi911/reflect" +CLONE_DIR="${HOME}/.local/share/reflect" + +info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +err() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; } + +command_exists() { command -v "$1" >/dev/null 2>&1; } + +# ── Preflight ────────────────────────────────────────────────────── +command_exists git || err "git is required. Install it and try again." +command_exists python3 || err "python3 is required (3.11+). Install it and try again." + +# ── Clone or update ──────────────────────────────────────────────── +if [ -d "$CLONE_DIR" ]; then + info "Updating existing install..." + git -C "$CLONE_DIR" pull --ff-only origin main 2>/dev/null || { + info "Pull failed, re-cloning..." + rm -rf "$CLONE_DIR" + git clone "https://github.com/${REPO}.git" "$CLONE_DIR" + } +else + info "Cloning reflect..." + git clone "https://github.com/${REPO}.git" "$CLONE_DIR" fi -echo "Skill installed: $SKILL_DST/SKILL.md" +# ── Install via pip ──────────────────────────────────────────────── +info "Installing reflect CLI..." +if command_exists pipx; then + pipx install --force "$CLONE_DIR" +elif command_exists pip; then + pip install --user "$CLONE_DIR" +else + # Last resort: install pip via ensurepip, then install + python3 -m ensurepip --default-pip 2>/dev/null || true + python3 -m pip install --user "$CLONE_DIR" || err "Could not install. Please install pip or pipx and try again." +fi -# ── Summary ────────────────────────────────────────────────────────── -echo "" -echo "Make sure $BIN_DIR is on your PATH." -echo "Run 'reflect init' in any git repo to get started." +# ── Verify ───────────────────────────────────────────────────────── +if command_exists reflect; then + info "reflect installed successfully!" + echo "" + reflect --help | head -3 +else + info "Install complete. Add ~/.local/bin to your PATH:" + echo "" + echo ' export PATH="$HOME/.local/bin:$PATH"' + echo "" + echo "Then restart your shell and run: reflect --help" +fi diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..65d6dbf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=68.0", "setuptools-scm>=8.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "reflect-cli" +dynamic = ["version"] +description = "Repo-owned memory for AI coding agents" +readme = "README.md" +license = "MIT" +requires-python = ">=3.11" +authors = [{ name = "codeyogi911" }] +keywords = ["ai", "agents", "memory", "context", "cli"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Version Control :: Git", +] + +[project.urls] +Homepage = "https://github.com/codeyogi911/reflect" +Repository = "https://github.com/codeyogi911/reflect" +Issues = "https://github.com/codeyogi911/reflect/issues" + +[project.scripts] +reflect = "reflect_cli.cli:main" + +[tool.setuptools.packages.find] +include = ["reflect_cli*"] + +[tool.setuptools_scm] +# Version derived from git tags (e.g. v0.1.0) +fallback_version = "0.0.0" diff --git a/reflect b/reflect index f663f8a..ba19f1c 100755 --- a/reflect +++ b/reflect @@ -13,15 +13,15 @@ import os # Use realpath to resolve symlinks (install.sh symlinks into ~/.local/bin) sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) -from lib.init import cmd_init -from lib.context import cmd_context -from lib.why import cmd_why -from lib.search import cmd_search -from lib.status import cmd_status -from lib.improve import cmd_improve -from lib.sessions import cmd_sessions -from lib.timeline import cmd_timeline -from lib.metrics import cmd_metrics +from reflect_cli.init import cmd_init +from reflect_cli.context import cmd_context +from reflect_cli.why import cmd_why +from reflect_cli.search import cmd_search +from reflect_cli.status import cmd_status +from reflect_cli.improve import cmd_improve +from reflect_cli.sessions import cmd_sessions +from reflect_cli.timeline import cmd_timeline +from reflect_cli.metrics import cmd_metrics def main(): diff --git a/lib/__init__.py b/reflect_cli/__init__.py similarity index 100% rename from lib/__init__.py rename to reflect_cli/__init__.py diff --git a/reflect_cli/__main__.py b/reflect_cli/__main__.py new file mode 100644 index 0000000..0605340 --- /dev/null +++ b/reflect_cli/__main__.py @@ -0,0 +1,6 @@ +"""Allow running as: python -m reflect_cli""" + +import sys +from reflect_cli.cli import main + +sys.exit(main() or 0) diff --git a/lib/aggregates.py b/reflect_cli/aggregates.py similarity index 100% rename from lib/aggregates.py rename to reflect_cli/aggregates.py diff --git a/reflect_cli/cli.py b/reflect_cli/cli.py new file mode 100644 index 0000000..361e4e5 --- /dev/null +++ b/reflect_cli/cli.py @@ -0,0 +1,103 @@ +"""reflect CLI entry point for installed package.""" + +import argparse +import sys + +from reflect_cli.init import cmd_init +from reflect_cli.context import cmd_context +from reflect_cli.why import cmd_why +from reflect_cli.search import cmd_search +from reflect_cli.status import cmd_status +from reflect_cli.improve import cmd_improve +from reflect_cli.sessions import cmd_sessions +from reflect_cli.timeline import cmd_timeline +from reflect_cli.metrics import cmd_metrics + + +def main(): + parser = argparse.ArgumentParser( + prog="reflect", + description="Repo-owned memory for AI coding agents.", + ) + subparsers = parser.add_subparsers(dest="command") + + # reflect init + init_parser = subparsers.add_parser("init", help="Initialize .reflect/ with default format") + init_parser.add_argument("--migrate", action="store_true", help="Migrate from legacy harness to format.yaml") + + # reflect context + ctx = subparsers.add_parser("context", help="Generate context.md via subagent synthesis") + ctx.add_argument("--max-lines", type=int, default=None, help="Line budget override") + + # reflect why + why = subparsers.add_parser("why", help="Answer questions about project history") + why.add_argument("query", nargs="+", help="File path or topic to search for") + why.add_argument("--raw", action="store_true", help="Dump raw evidence without synthesis") + why.add_argument("--verbose", "-v", action="store_true", help="Show maker-checker progress and raw evidence") + + # reflect search + search = subparsers.add_parser("search", help="Grep across all evidence sources") + search.add_argument("query", nargs="+", help="Search query") + + # reflect status + subparsers.add_parser("status", help="Show evidence source availability") + + # reflect sessions + sess = subparsers.add_parser("sessions", help="List and inspect Entire CLI sessions") + sess.add_argument("session_id", nargs="?", default=None, help="Session ID for detail view") + sess.add_argument("--limit", type=int, default=15, help="Number of sessions to show (default: 15)") + + # reflect timeline + tl = subparsers.add_parser("timeline", help="Date-grouped view of sessions and checkpoints") + tl.add_argument("--days", type=int, default=7, help="Number of days to show (default: 7)") + tl.add_argument("--json", action="store_true", help="Output as JSON") + + # reflect improve + subparsers.add_parser("improve", help="Analyze context quality, suggest format.yaml changes") + + # reflect metrics + met = subparsers.add_parser( + "metrics", + help="Print metrics JSON and/or export shields.io badge endpoint files", + ) + met.add_argument( + "--export", + metavar="DIR", + dest="export_dir", + default=None, + help="Write shields endpoint JSON files into DIR", + ) + met.add_argument( + "--no-json", + action="store_true", + help="Do not print JSON to stdout (use with --export)", + ) + met.add_argument( + "--generate-summaries", + action="store_true", + help="Allow Entire to generate missing summaries (slow; default off)", + ) + + args = parser.parse_args() + + if args.command is None: + parser.print_help() + return 0 + + commands = { + "init": cmd_init, + "context": cmd_context, + "why": cmd_why, + "search": cmd_search, + "status": cmd_status, + "sessions": cmd_sessions, + "timeline": cmd_timeline, + "improve": cmd_improve, + "metrics": cmd_metrics, + } + + return commands[args.command](args) + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/lib/context.py b/reflect_cli/context.py similarity index 99% rename from lib/context.py rename to reflect_cli/context.py index 3743578..9978d9f 100644 --- a/lib/context.py +++ b/reflect_cli/context.py @@ -13,7 +13,7 @@ from datetime import datetime from pathlib import Path -from lib.evidence import gather_evidence, build_evidence_document, truncate_evidence +from reflect_cli.evidence import gather_evidence, build_evidence_document, truncate_evidence # --------------------------------------------------------------------------- diff --git a/lib/evidence.py b/reflect_cli/evidence.py similarity index 99% rename from lib/evidence.py rename to reflect_cli/evidence.py index e953517..d0b29ea 100644 --- a/lib/evidence.py +++ b/reflect_cli/evidence.py @@ -11,7 +11,7 @@ from collections import defaultdict from pathlib import Path -from lib.sources import run, has_entire, has_git +from reflect_cli.sources import run, has_entire, has_git def gather_evidence(max_checkpoints=12, auto_generate=True): diff --git a/lib/improve.py b/reflect_cli/improve.py similarity index 98% rename from lib/improve.py rename to reflect_cli/improve.py index d69a051..154456d 100644 --- a/lib/improve.py +++ b/reflect_cli/improve.py @@ -9,8 +9,8 @@ import sys from pathlib import Path -from lib.evidence import gather_evidence -from lib.context import load_format +from reflect_cli.evidence import gather_evidence +from reflect_cli.context import load_format def analyze_context_quality(context_md): diff --git a/lib/init.py b/reflect_cli/init.py similarity index 100% rename from lib/init.py rename to reflect_cli/init.py diff --git a/lib/metrics.py b/reflect_cli/metrics.py similarity index 100% rename from lib/metrics.py rename to reflect_cli/metrics.py diff --git a/lib/search.py b/reflect_cli/search.py similarity index 100% rename from lib/search.py rename to reflect_cli/search.py diff --git a/lib/sessions.py b/reflect_cli/sessions.py similarity index 100% rename from lib/sessions.py rename to reflect_cli/sessions.py diff --git a/lib/sources.py b/reflect_cli/sources.py similarity index 100% rename from lib/sources.py rename to reflect_cli/sources.py diff --git a/lib/status.py b/reflect_cli/status.py similarity index 100% rename from lib/status.py rename to reflect_cli/status.py diff --git a/lib/synthesize.py b/reflect_cli/synthesize.py similarity index 100% rename from lib/synthesize.py rename to reflect_cli/synthesize.py diff --git a/lib/timeline.py b/reflect_cli/timeline.py similarity index 100% rename from lib/timeline.py rename to reflect_cli/timeline.py diff --git a/lib/why.py b/reflect_cli/why.py similarity index 100% rename from lib/why.py rename to reflect_cli/why.py diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 11ac154..d4a68fd 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -8,7 +8,7 @@ trap 'rm -rf "$TMP"' EXIT cp "$ROOT/reflect" "$TMP/reflect" chmod +x "$TMP/reflect" -cp -R "$ROOT/lib" "$TMP/lib" +cp -R "$ROOT/reflect_cli" "$TMP/reflect_cli" cd "$TMP" git init -q