From dd7e6a1562b63b9cfbadd4429d760943e7d8d525 Mon Sep 17 00:00:00 2001 From: Amit Patnaik Date: Thu, 30 Apr 2026 04:56:34 +0530 Subject: [PATCH 1/2] Preserve clean mode for last run state --- hooks/scripts/check-config.sh | 42 +++++++++++++ skills/last30days/scripts/last30days.py | 20 ++++++ tests/test_last_run_state.py | 84 +++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 tests/test_last_run_state.py diff --git a/hooks/scripts/check-config.sh b/hooks/scripts/check-config.sh index bb20a6e..a9c7f69 100755 --- a/hooks/scripts/check-config.sh +++ b/hooks/scripts/check-config.sh @@ -63,14 +63,52 @@ fi # Check SETUP_COMPLETE (from file or env) SETUP_COMPLETE="${ENV_SETUP_COMPLETE:-${SETUP_COMPLETE:-}}" +# Compute last-run summary line (if last-run.json exists) +if [[ "${LAST30DAYS_CONFIG_DIR+x}" == "x" ]]; then + if [[ -n "$LAST30DAYS_CONFIG_DIR" ]]; then + LAST_RUN_FILE="$LAST30DAYS_CONFIG_DIR/last-run.json" + else + LAST_RUN_FILE="" + fi +else + LAST_RUN_FILE="$HOME/.config/last30days/last-run.json" +fi +LAST_RUN_LINE="" +if [[ -n "$LAST_RUN_FILE" && -f "$LAST_RUN_FILE" ]]; then + LAST_RUN_LINE=$(LAST_RUN_FILE="$LAST_RUN_FILE" python3 - <<'PY' 2>/dev/null +import datetime +import json +import os + +path = os.environ["LAST_RUN_FILE"] +try: + d = json.load(open(path)) + topic = (d.get("topic") or "?")[:60] + ts = d.get("timestamp", "") + dt = datetime.datetime.fromisoformat(ts.replace("Z", "+00:00")) + delta = (datetime.datetime.now(datetime.timezone.utc) - dt).total_seconds() + if delta < 60: ago = f"{int(delta)}s ago" + elif delta < 3600: ago = f"{int(delta//60)}m ago" + elif delta < 86400: ago = f"{int(delta//3600)}h ago" + else: ago = f"{int(delta//86400)}d ago" + total = d.get("total", 0) + print(f" Last run: \"{topic}\" · {ago} · {total} results") +except Exception: + pass +PY +) +fi + # If setup has never been run, show welcome message for new users if [[ -z "$SETUP_COMPLETE" && -z "$CONFIG_FILE" && -z "${OPENAI_API_KEY:-}" && -z "${SCRAPECREATORS_API_KEY:-}" && -z "${AUTH_TOKEN:-}" && -z "${XAI_API_KEY:-}" ]]; then cat <<'EOF' /last30days: Ready to use. Run /last30days to get started — setup takes 30 seconds. + Research any topic across Reddit, HN, X, YouTube, Polymarket (last 30 days). Reddit, Hacker News, and Polymarket work out of the box. The setup wizard can unlock X/Twitter, YouTube, and more. EOF + [[ -n "$LAST_RUN_LINE" ]] && echo "$LAST_RUN_LINE" exit 0 fi @@ -121,9 +159,13 @@ fi if [[ -n "$HAS_SCRAPECREATORS" ]]; then # Fully configured — compact ready message echo "/last30days: Ready — ${SOURCE_COUNT} sources active." + echo " Research any topic across social + market + web sources (last 30 days)." + [[ -n "$LAST_RUN_LINE" ]] && echo "$LAST_RUN_LINE" else # Setup done but missing ScrapeCreators — recommend it echo "/last30days: Ready — ${SOURCE_COUNT} sources active." + echo " Research any topic across social + market + web sources (last 30 days)." + [[ -n "$LAST_RUN_LINE" ]] && echo "$LAST_RUN_LINE" echo " Tip: Add ScrapeCreators for Reddit comments + TikTok + Instagram." echo " 100 free credits, no credit card — scrapecreators.com" echo " last30days has no affiliation with any API provider." diff --git a/skills/last30days/scripts/last30days.py b/skills/last30days/scripts/last30days.py index b22d28f..d2df7fa 100644 --- a/skills/last30days/scripts/last30days.py +++ b/skills/last30days/scripts/last30days.py @@ -531,6 +531,25 @@ def _show_runtime_ui( progress.show_promo(promo, diag=diag) +def _write_last_run(topic: str, report: "schema.Report") -> None: + try: + import datetime + if env.CONFIG_DIR is None: + return + target = env.CONFIG_DIR + target.mkdir(parents=True, exist_ok=True) + counts = {source: len(items) for source, items in report.items_by_source.items()} + payload = { + "topic": topic, + "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "sources": counts, + "total": sum(counts.values()), + } + (target / "last-run.json").write_text(json.dumps(payload, indent=2)) + except Exception: + pass + + def main() -> int: parser = build_parser() # Use parse_known_args so setup sub-flags (--device-auth, --github, @@ -870,6 +889,7 @@ def main() -> int: report, progress, diag, suppress_web_promo=bool(external_plan or comp_plan), ) + _write_last_run(topic, report) if args.store: counts = persist_report(report) sys.stderr.write( diff --git a/tests/test_last_run_state.py b/tests/test_last_run_state.py new file mode 100644 index 0000000..2a28eac --- /dev/null +++ b/tests/test_last_run_state.py @@ -0,0 +1,84 @@ +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +LAST30DAYS_SCRIPT = REPO_ROOT / "skills" / "last30days" / "scripts" / "last30days.py" + + +def run_last30days(topic: str, env: dict[str, str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(LAST30DAYS_SCRIPT), topic, "--mock", "--emit=json"], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + +class LastRunStateTests(unittest.TestCase): + def test_empty_config_override_disables_last_run_write(self): + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) / "home" + env = os.environ.copy() + env["HOME"] = str(home) + env["LAST30DAYS_CONFIG_DIR"] = "" + + result = run_last30days("synthetic eval query", env) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse((home / ".config" / "last30days" / "last-run.json").exists()) + + def test_custom_config_override_writes_last_run_to_custom_dir(self): + with tempfile.TemporaryDirectory() as tmp: + config_dir = Path(tmp) / "custom-config" + env = os.environ.copy() + env["HOME"] = str(Path(tmp) / "home") + env["LAST30DAYS_CONFIG_DIR"] = str(config_dir) + + result = run_last30days("custom config query", env) + + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads((config_dir / "last-run.json").read_text()) + self.assertEqual(payload["topic"], "custom config query") + self.assertGreaterEqual(payload["total"], 0) + + def test_hook_reads_last_run_from_custom_config_dir(self): + with tempfile.TemporaryDirectory() as tmp: + config_dir = Path(tmp) / "custom-config" + config_dir.mkdir() + (config_dir / "last-run.json").write_text( + json.dumps( + { + "topic": "custom hook query", + "timestamp": "2026-04-30T00:00:00+00:00", + "sources": {"reddit": 2}, + "total": 2, + } + ) + ) + env = os.environ.copy() + env["HOME"] = str(Path(tmp) / "home") + env["LAST30DAYS_CONFIG_DIR"] = str(config_dir) + + result = subprocess.run( + ["bash", "hooks/scripts/check-config.sh"], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('Last run: "custom hook query"', result.stdout) + + +if __name__ == "__main__": + unittest.main() From b296a65515310373082f55e9dfae09840a16c1fe Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Sun, 17 May 2026 00:44:30 -0700 Subject: [PATCH 2/2] fix(last-run): guard python3 absence + hoist datetime + use context manager --- hooks/scripts/check-config.sh | 7 ++++--- skills/last30days/scripts/last30days.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/hooks/scripts/check-config.sh b/hooks/scripts/check-config.sh index a9c7f69..4189027 100755 --- a/hooks/scripts/check-config.sh +++ b/hooks/scripts/check-config.sh @@ -74,15 +74,16 @@ else LAST_RUN_FILE="$HOME/.config/last30days/last-run.json" fi LAST_RUN_LINE="" -if [[ -n "$LAST_RUN_FILE" && -f "$LAST_RUN_FILE" ]]; then - LAST_RUN_LINE=$(LAST_RUN_FILE="$LAST_RUN_FILE" python3 - <<'PY' 2>/dev/null +if [[ -n "$LAST_RUN_FILE" && -f "$LAST_RUN_FILE" ]] && command -v python3 &>/dev/null; then + LAST_RUN_LINE=$(LAST_RUN_FILE="$LAST_RUN_FILE" python3 - <<'PY' 2>/dev/null || true import datetime import json import os path = os.environ["LAST_RUN_FILE"] try: - d = json.load(open(path)) + with open(path) as fh: + d = json.load(fh) topic = (d.get("topic") or "?")[:60] ts = d.get("timestamp", "") dt = datetime.datetime.fromisoformat(ts.replace("Z", "+00:00")) diff --git a/skills/last30days/scripts/last30days.py b/skills/last30days/scripts/last30days.py index d2df7fa..0a870cc 100644 --- a/skills/last30days/scripts/last30days.py +++ b/skills/last30days/scripts/last30days.py @@ -6,6 +6,7 @@ from __future__ import annotations import argparse import atexit +import datetime import json import os import re @@ -533,7 +534,6 @@ def _show_runtime_ui( def _write_last_run(topic: str, report: "schema.Report") -> None: try: - import datetime if env.CONFIG_DIR is None: return target = env.CONFIG_DIR