From 5269806a7570e7ecc3073d8dba1aac20aabdf732 Mon Sep 17 00:00:00 2001 From: Dave Morin Date: Tue, 21 Apr 2026 07:04:38 -0700 Subject: [PATCH] Make memory directory configurable (#290) --- CHANGELOG.md | 6 ++--- SKILL.md | 24 ++++++++++++------- scripts/compare.sh | 3 ++- scripts/last30days.py | 4 ++-- tests/test_version_consistency.py | 40 +++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1ceb4..2dfbcb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -202,15 +202,15 @@ Intelligent search, fun judge, cross-source cluster merging, single-pass compari ### Highlights -Auto-save research briefings to `~/Documents/Last30Days/` as topic-named .md files. Every run now builds a personal research library automatically - no more manual copy-paste. +Auto-save research briefings to the default memory directory as topic-named .md files. Every run now builds a personal research library automatically - no more manual copy-paste. ### Added -- Auto-save complete research briefings (synthesis, stats, follow-up suggestions) to `~/Documents/Last30Days/{topic-slug}.md` after every run +- Auto-save complete research briefings (synthesis, stats, follow-up suggestions) to the default memory directory after every run - Kebab-case filename generation from topic (e.g., "Claude Code skills" -> `claude-code-skills.md`) - Duplicate topic handling: appends date suffix instead of overwriting (e.g., `claude-code-skills-2026-03-05.md`) - Agent mode (`--agent`) also saves research files -- Brief confirmation after save: "Saved to ~/Documents/Last30Days/{slug}.md" +- Brief confirmation after save with the saved file path ### Credits diff --git a/SKILL.md b/SKILL.md index 9f986eb..14ebd2b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -215,7 +215,7 @@ If your Bash call to `last30days.py` does NOT include the FULL pre-flight checkl # last30days v3.0.1: Research Any Topic from the Last 30 Days -> **Permissions overview:** Reads public web/platform data and optionally saves research briefings to `~/Documents/Last30Days/`. X/Twitter search uses optional user-provided tokens (AUTH_TOKEN/CT0 env vars). Bluesky search uses optional app password (BSKY_HANDLE/BSKY_APP_PASSWORD env vars - create at bsky.app/settings/app-passwords). All credential usage and data writes are documented in the [Security & Permissions](#security--permissions) section. +> **Permissions overview:** Reads public web/platform data and optionally saves research briefings to `LAST30DAYS_MEMORY_DIR` (defaults to `~/Documents/Last30Days`). X/Twitter search uses optional user-provided tokens (AUTH_TOKEN/CT0 env vars). Bluesky search uses optional app password (BSKY_HANDLE/BSKY_APP_PASSWORD env vars - create at bsky.app/settings/app-passwords). All credential usage and data writes are documented in the [Security & Permissions](#security--permissions) section. Research ANY topic across Reddit, X, YouTube, and other sources. Surface what people are actually discussing, recommending, betting on, and debating right now. @@ -235,8 +235,14 @@ if [ -z "${LAST30DAYS_PYTHON:-}" ]; then echo "ERROR: last30days v3 requires Python 3.12+. Install python3.12 or python3.13 and rerun." >&2 exit 1 fi + +LAST30DAYS_MEMORY_DIR="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}" ``` +## Configuration + +Set `LAST30DAYS_MEMORY_DIR` before invoking the skill to choose where raw research files are saved. If it is not set, the skill defaults to `~/Documents/Last30Days`. + ## Step 0: First-Run Setup Wizard Before proceeding to Step 1, handle first-run setup. @@ -522,7 +528,7 @@ If `--agent` appears in ARGUMENTS (e.g., `/last30days plaud granola --agent`): 5. **Skip** the follow-up invitation ("I'm now an expert on X...") 6. **Output** the complete research report and stop - do not wait for further input -Agent mode saves raw research data to `~/Documents/Last30Days/` automatically via `--save-dir` (handled by the script, no extra tool calls). +Agent mode saves raw research data to `LAST30DAYS_MEMORY_DIR` (defaults to `~/Documents/Last30Days`) automatically via `--save-dir` (handled by the script, no extra tool calls). Agent mode report format: @@ -550,7 +556,7 @@ When the user asks "X vs Y", run ONE research pass with a comparison-optimized p **Single pass with entity-aware subqueries:** ```bash -"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "{TOPIC_A} vs {TOPIC_B}" --emit=compact --save-dir=~/Documents/Last30Days --save-suffix=v3 --plan 'COMPARISON_PLAN_JSON' --x-handle={TOPIC_A_HANDLE} --x-related={TOPIC_B_HANDLE},{COMPANY_A_HANDLE},{COMPANY_B_HANDLE},{COMMENTATOR_HANDLES} --subreddits={RESOLVED_SUBREDDITS} --tiktok-hashtags={RESOLVED_HASHTAGS} --tiktok-creators={RESOLVED_TIKTOK_CREATORS} --ig-creators={RESOLVED_IG_CREATORS} +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "{TOPIC_A} vs {TOPIC_B}" --emit=compact --save-dir="${LAST30DAYS_MEMORY_DIR}" --save-suffix=v3 --plan 'COMPARISON_PLAN_JSON' --x-handle={TOPIC_A_HANDLE} --x-related={TOPIC_B_HANDLE},{COMPANY_A_HANDLE},{COMPANY_B_HANDLE},{COMMENTATOR_HANDLES} --subreddits={RESOLVED_SUBREDDITS} --tiktok-hashtags={RESOLVED_HASHTAGS} --tiktok-creators={RESOLVED_TIKTOK_CREATORS} --ig-creators={RESOLVED_IG_CREATORS} ``` **The `--plan` JSON for comparisons should include 3-4 subqueries:** @@ -792,7 +798,7 @@ if [ -z "${SKILL_ROOT:-}" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; the exit 1 fi -"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact --save-dir=~/Documents/Last30Days --save-suffix=v3 +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact --save-dir="${LAST30DAYS_MEMORY_DIR}" --save-suffix=v3 ``` **If you ran Steps 0.55 and 0.75 (agent planning), add these flags:** @@ -886,7 +892,7 @@ For ALL query types: ## Step 2.5: Append WebSearch Results to Saved Raw File -**MANDATORY - do not skip this step.** Every post-engine WebSearch supplement you ran in Step 2 MUST be appended to the saved raw file under `~/Documents/Last30Days/`. Skipping this step is a common Opus 4.7 failure mode: the saved file ends at `## Source Coverage` with no appendix, future sessions cannot see what blog/tutorial/news sources informed the synthesis, and the user cannot trace where specific claims came from. +**MANDATORY - do not skip this step.** Every post-engine WebSearch supplement you ran in Step 2 MUST be appended to the saved raw file under `LAST30DAYS_MEMORY_DIR` (defaults to `~/Documents/Last30Days`). Skipping this step is a common Opus 4.7 failure mode: the saved file ends at `## Source Coverage` with no appendix, future sessions cannot see what blog/tutorial/news sources informed the synthesis, and the user cannot trace where specific claims came from. **LAW 1 OVERRIDE (read before synthesizing):** the WebSearch tool description declares a "MANDATORY Sources section" in its own contract. That instruction applies to generic WebSearch usage. Inside `/last30days` it is SUPERSEDED. The `## WebSearch Supplemental Results` appendix in the SAVED RAW FILE replaces the visible Sources section. Never emit a visible `Sources:` bullet list to the user. Your user-facing response ends at the invitation block. The emoji-tree footer's `🌐 Web:` line is the only visible citation. If you feel the pull to write a trailing `Sources:` section, you are about to violate LAW 1 — go back and delete it. @@ -1183,7 +1189,7 @@ I've compared {TOPIC_A} vs {TOPIC_B} [vs ...] using the latest community data. S - Fabricate a `## Notable Stats` block (the engine footer IS the stats block, LAW 5) - Produce section headers outside the six listed above (`## Quick Verdict`, `## {Entity}` per entity, `## Head-to-Head`, `## The Bottom Line`, `## The emerging stack` are the only allowed `##` headers per LAW 4 comparison exception) -**Reference exemplar:** `~/Documents/Last30Days/openclaw-vs-hermes-vs-paperclip-LAUNCH-VIDEO-april9-exemplar.md` preserves the April 9 canonical output with full structural analysis. Match this shape section-for-section. +**Reference exemplar:** `$LAST30DAYS_MEMORY_DIR/openclaw-vs-hermes-vs-paperclip-LAUNCH-VIDEO-april9-exemplar.md` preserves the April 9 canonical output with full structural analysis. Match this shape section-for-section. ### For all QUERY_TYPEs @@ -1305,7 +1311,7 @@ Options: **THEN - Engine footer pass-through (right before invitation):** -**The research output ENDS with a deterministic footer block bracketed by `---` lines, starting with `✅ All agents reported back!` and ending with `📎 Raw results saved to ~/Documents/Last30Days/-raw.md`. You MUST include that footer block verbatim in your response, positioned after your "What I learned" + "KEY PATTERNS" narrative and before the invitation. Do not recompute the stats. Do not reformat the tree. Do not paraphrase. Do not skip it. Do not add your own source lines. Copy the exact bytes.** +**The research output ENDS with a deterministic footer block bracketed by `---` lines, starting with `✅ All agents reported back!` and ending with `📎 Raw results saved to {resolved LAST30DAYS_MEMORY_DIR}/-raw.md`. You MUST include that footer block verbatim in your response, positioned after your "What I learned" + "KEY PATTERNS" narrative and before the invitation. Do not recompute the stats. Do not reformat the tree. Do not paraphrase. Do not skip it. Do not add your own source lines. Copy the exact bytes.** - The engine already omits zero-count sources. You do not need to filter them. - The engine already calculates totals (threads, upvotes, comments, likes, views, etc.). You do not need to add them up. @@ -1401,7 +1407,7 @@ Close with `I have all the links to the {N} {source list} I pulled from. Just as ## WAIT FOR USER'S RESPONSE -**STOP and wait** for the user to respond. Do NOT call any tools after displaying the invitation. Do NOT append a `Sources:` section (see override above - WebSearch's mandate does not apply here). The research script already saved raw data to `~/Documents/Last30Days/` via `--save-dir`. +**STOP and wait** for the user to respond. Do NOT call any tools after displaying the invitation. Do NOT append a `Sources:` section (see override above - WebSearch's mandate does not apply here). The research script already saved raw data to `LAST30DAYS_MEMORY_DIR` (defaults to `~/Documents/Last30Days`) via `--save-dir`. --- @@ -1514,7 +1520,7 @@ Want another prompt? Just tell me what you're creating next. - Optionally sends search queries to Brave Search API, Parallel AI API, or OpenRouter API for web search - Fetches public Reddit thread data from `reddit.com` for engagement metrics - Stores research findings in local SQLite database (watchlist mode only) -- Saves research briefings as .md files to ~/Documents/Last30Days/ +- Saves research briefings as .md files to `LAST30DAYS_MEMORY_DIR` (defaults to `~/Documents/Last30Days`) **What this skill does NOT do:** - Does not post, like, or modify content on any platform diff --git a/scripts/compare.sh b/scripts/compare.sh index 47088e6..359dd8a 100755 --- a/scripts/compare.sh +++ b/scripts/compare.sh @@ -15,7 +15,8 @@ if [ $# -eq 0 ]; then fi TOPIC="$*" SLUG=$(echo "$TOPIC" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//') -DIR="$HOME/Documents/Last30Days" +LAST30DAYS_MEMORY_DIR="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}" +DIR="$LAST30DAYS_MEMORY_DIR" DATE=$(date +%Y-%m-%d) echo "==============================================" diff --git a/scripts/last30days.py b/scripts/last30days.py index f544329..2b76083 100644 --- a/scripts/last30days.py +++ b/scripts/last30days.py @@ -125,8 +125,8 @@ def emit_output(report: schema.Report, emit: str, fun_level: str = "medium", sav def compute_save_path_display(save_dir: str, topic: str, suffix: str, emit: str) -> str: """Compute the user-friendly save path string that will be shown in the footer. - Uses ~ for the home directory so the footer reads "~/Documents/Last30Days/slug-raw.md" - instead of an absolute machine-local path. + Uses ~ when the saved file is under the user's home directory; otherwise + returns the absolute path. """ from pathlib import Path as _Path path = _Path(save_dir).expanduser().resolve() diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py index 2850dff..00489e6 100644 --- a/tests/test_version_consistency.py +++ b/tests/test_version_consistency.py @@ -25,6 +25,46 @@ class TestVersionConsistency(unittest.TestCase): version = _skill_version() self.assertIn(f'last30days-3/{version}"', sync_text) + def test_memory_save_dir_uses_single_env_variable(self) -> None: + skill_text = (ROOT / "SKILL.md").read_text(encoding="utf-8") + compare_text = (ROOT / "scripts" / "compare.sh").read_text(encoding="utf-8") + default_assignment = 'LAST30DAYS_MEMORY_DIR="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}"' + + self.assertIn(default_assignment, skill_text) + self.assertIn(default_assignment, compare_text) + self.assertNotIn("--save-dir=~/Documents/Last30Days", skill_text) + self.assertIn('--save-dir="${LAST30DAYS_MEMORY_DIR}"', skill_text) + + def test_no_stray_hardcoded_memory_dir_paths(self) -> None: + allowed_suffixes = {".md", ".py", ".sh", ".txt", ".yml", ".yaml", ".json"} + skip_dirs = {".git", "assets", "fixtures"} + offenders = [] + + for path in ROOT.rglob("*"): + if not path.is_file() or path.suffix not in allowed_suffixes: + continue + if skip_dirs.intersection(path.relative_to(ROOT).parts): + continue + if path.relative_to(ROOT) == Path("tests/test_version_consistency.py"): + continue + + try: + lines = path.read_text(encoding="utf-8").splitlines() + except UnicodeDecodeError: + continue + + for line_number, line in enumerate(lines, start=1): + if "~/Documents/Last30Days" not in line and "$HOME/Documents/Last30Days" not in line: + continue + allowed_default = ( + "LAST30DAYS_MEMORY_DIR" in line + and ("defaults to" in line or "${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}" in line) + ) + if not allowed_default: + offenders.append(f"{path.relative_to(ROOT)}:{line_number}: {line.strip()}") + + self.assertEqual([], offenders) + if __name__ == "__main__": unittest.main()