Compare commits
2 Commits
v2.9.4
..
temp-tweet
| Author | SHA1 | Date | |
|---|---|---|---|
| 69aa98f3f8 | |||
| 6edb813bea |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "last30days",
|
"name": "last30days",
|
||||||
"description": "Research any topic from the last 30 days across Reddit, X, YouTube, and the web",
|
"description": "Research any topic from the last 30 days across Reddit, X, YouTube, and the web",
|
||||||
"version": "2.9.1",
|
"version": "2.1.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "mvanhorn"
|
"name": "mvanhorn"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,47 +5,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [2.9.4] - 2026-03-06
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Move save into Python script via `--save-dir` flag - raw research data saved during the existing script Bash call, zero extra tool calls after invitation
|
|
||||||
- Remove entire "Save Research to Documents" section from SKILL.md (~45 lines removed)
|
|
||||||
- No more `📎` footer, no Bash heredoc, no `(No output)`, no multi-minute cogitation after research
|
|
||||||
|
|
||||||
## [2.9.3] - 2026-03-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- **Critical:** Switch save from `run_in_background` to foreground Bash - background callbacks caused model to re-engage, hallucinate fake user messages, and generate unsolicited multi-paragraph responses
|
|
||||||
- Save uses foreground `cat >` heredoc (executes sub-second, no callback, no delayed notification)
|
|
||||||
|
|
||||||
## [2.9.2] - 2026-03-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Save research silently using background Bash heredoc instead of Write tool (eliminates "Wrote N lines..." clutter)
|
|
||||||
- Suppress follow-up text after background save completes (no more "Research briefing saved..." noise)
|
|
||||||
- Add `📎` footer line for save path instead of verbose confirmation
|
|
||||||
|
|
||||||
## [2.9.1] - 2026-03-05
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Auto-save complete research briefings (synthesis, stats, follow-up suggestions) to `~/Documents/Last30Days/{topic-slug}.md` 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"
|
|
||||||
|
|
||||||
### Credits
|
|
||||||
|
|
||||||
- [@devin_explores](https://x.com/devin_explores) -- Inspired this feature by sharing their workflow of saving every last30days run into organized .md files ([PR #51](https://github.com/mvanhorn/last30days-skill/pull/51))
|
|
||||||
|
|
||||||
## [2.9.0] - 2026-03-05
|
## [2.9.0] - 2026-03-05
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
@@ -155,7 +114,6 @@ Three headline features: watchlists for always-on bots, YouTube transcripts as a
|
|||||||
|
|
||||||
Initial public release. Reddit + X search via OpenAI Responses API and xAI API.
|
Initial public release. Reddit + X search via OpenAI Responses API and xAI API.
|
||||||
|
|
||||||
[2.9.1]: https://github.com/mvanhorn/last30days-skill/compare/v2.9.0...v2.9.1
|
|
||||||
[2.9.0]: https://github.com/mvanhorn/last30days-skill/compare/v2.8.0...v2.9.0
|
[2.9.0]: https://github.com/mvanhorn/last30days-skill/compare/v2.8.0...v2.9.0
|
||||||
[2.8.0]: https://github.com/mvanhorn/last30days-skill/compare/v2.6.0...v2.8.0
|
[2.8.0]: https://github.com/mvanhorn/last30days-skill/compare/v2.6.0...v2.8.0
|
||||||
[2.1.0]: https://github.com/mvanhorn/last30days-skill/compare/v1.0.0...v2.1.0
|
[2.1.0]: https://github.com/mvanhorn/last30days-skill/compare/v1.0.0...v2.1.0
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
# /last30days v2.9.1
|
# /last30days v2.9
|
||||||
|
|
||||||
**The AI world reinvents itself every month. This skill keeps you current.** /last30days researches your topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations. Whether it's Seedance 2.0 access, paper.design prompts, or the latest Nano Banana Pro techniques, you'll know what people who are paying attention already know.
|
**The AI world reinvents itself every month. This skill keeps you current.** /last30days researches your topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations. Whether it's Seedance 2.0 access, paper.design prompts, or the latest Nano Banana Pro techniques, you'll know what people who are paying attention already know.
|
||||||
|
|
||||||
**New in v2.9.1 — Auto-save to ~/Documents/Last30Days/:** Every run now saves the complete briefing as a topic-named `.md` file to your Documents folder. Build a personal research library automatically. Inspired by [@devin_explores](https://x.com/devin_explores).
|
|
||||||
|
|
||||||
**New in v2.9 — ScrapeCreators Reddit + Top Comments + Smart Discovery:**
|
**New in v2.9 — ScrapeCreators Reddit + Top Comments + Smart Discovery:**
|
||||||
|
|
||||||
Reddit now runs on [ScrapeCreators](https://scrapecreators.com) by default — one `SCRAPECREATORS_API_KEY` covers Reddit, TikTok, and Instagram (3 sources, 1 key). Smart subreddit discovery finds the right communities automatically, and top comments are elevated with a 10% scoring weight and `💬` display with upvote counts. [Details below.](#whats-new-in-v29)
|
Reddit now runs on [ScrapeCreators](https://scrapecreators.com) by default — one `SCRAPECREATORS_API_KEY` covers Reddit, TikTok, and Instagram (3 sources, 1 key). Smart subreddit discovery finds the right communities automatically, and top comments are elevated with a 10% scoring weight and `💬` display with upvote counts. [Details below.](#whats-new-in-v29)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: last30days
|
name: last30days
|
||||||
version: "2.9.2"
|
version: "2.9"
|
||||||
description: "Research a topic from the last 30 days. Also triggered by 'last30'. Sources: Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, web. Become an expert and write copy-paste-ready prompts."
|
description: "Research a topic from the last 30 days. Also triggered by 'last30'. Sources: Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, web. Become an expert and write copy-paste-ready prompts."
|
||||||
argument-hint: 'last30 AI video tools, last30 best project management tools'
|
argument-hint: 'last30 AI video tools, last30 best project management tools'
|
||||||
allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch
|
allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch
|
||||||
@@ -30,7 +30,7 @@ metadata:
|
|||||||
- prompts
|
- prompts
|
||||||
---
|
---
|
||||||
|
|
||||||
# last30days v2.9.4: Research Any Topic from the Last 30 Days
|
# last30days v2.9: Research Any Topic from the Last 30 Days
|
||||||
|
|
||||||
Research ANY topic across Reddit, X, YouTube, TikTok, Hacker News, Polymarket, and the web. Surface what people are actually discussing, recommending, betting on, and debating right now.
|
Research ANY topic across Reddit, X, YouTube, TikTok, Hacker News, Polymarket, and the web. Surface what people are actually discussing, recommending, betting on, and debating right now.
|
||||||
|
|
||||||
@@ -123,8 +123,6 @@ If `--agent` appears in ARGUMENTS (e.g., `/last30days plaud granola --agent`):
|
|||||||
5. **Skip** the follow-up invitation ("I'm now an expert on X...")
|
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
|
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 report format:
|
Agent mode report format:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -167,7 +165,7 @@ if [ -z "${SKILL_ROOT:-}" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact --no-native-web --save-dir=~/Documents/Last30Days # Add --x-handle=HANDLE if RESOLVED_HANDLE is set
|
python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact --no-native-web # Add --x-handle=HANDLE if RESOLVED_HANDLE is set
|
||||||
```
|
```
|
||||||
|
|
||||||
Use a **timeout of 300000** (5 minutes) on the Bash call. The script typically takes 1-3 minutes.
|
Use a **timeout of 300000** (5 minutes) on the Bash call. The script typically takes 1-3 minutes.
|
||||||
@@ -500,7 +498,7 @@ For `/last30days war in Iran` (NEWS):
|
|||||||
|
|
||||||
## WAIT FOR USER'S RESPONSE
|
## WAIT FOR USER'S RESPONSE
|
||||||
|
|
||||||
**STOP and wait** for the user to respond. Do NOT call any tools after displaying the invitation. The research script already saved raw data to `~/Documents/Last30Days/` via `--save-dir`.
|
After showing the stats summary with your invitation, **STOP and wait** for the user to respond.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -609,7 +607,6 @@ 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
|
- 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
|
- Fetches public Reddit thread data from `reddit.com` for engagement metrics
|
||||||
- Stores research findings in local SQLite database (watchlist mode only)
|
- Stores research findings in local SQLite database (watchlist mode only)
|
||||||
- Saves research briefings as .md files to ~/Documents/Last30Days/
|
|
||||||
|
|
||||||
**What this skill does NOT do:**
|
**What this skill does NOT do:**
|
||||||
- Does not post, like, or modify content on any platform
|
- Does not post, like, or modify content on any platform
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
---
|
|
||||||
title: "feat: Auto-save research results to ~/Documents/Last30Days/"
|
|
||||||
type: feat
|
|
||||||
status: completed
|
|
||||||
date: 2026-03-05
|
|
||||||
---
|
|
||||||
|
|
||||||
# feat: Auto-save research results to ~/Documents/Last30Days/
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Every time the last30days skill completes a research run, automatically save the full briefing - inquiry, synthesis, stats, and follow-up suggestions - as a topic-named `.md` file in `~/Documents/Last30Days/`. Inspired by how users like @devin_explores are already manually saving results to build a personal research library (see screenshot - 17 topic files in a `Last30Days` Finder folder, each 9-34 KB).
|
|
||||||
|
|
||||||
## Problem Statement / Motivation
|
|
||||||
|
|
||||||
The skill's most valuable output - the assistant's synthesized "What I learned" briefing with stats and citations - only exists in the conversation. Once the session ends, it's gone. Users like @devin_explores work around this by manually copying output into .md files. Meanwhile, the Python script already writes raw data to `~/.local/share/last30days/out/`, but:
|
|
||||||
|
|
||||||
1. It overwrites on every run (no history)
|
|
||||||
2. It only contains pre-synthesis data (scored items), not the assistant's expert briefing
|
|
||||||
3. It's in a hidden dot-directory users don't naturally browse
|
|
||||||
|
|
||||||
The feature makes saving automatic and puts files where users expect them - the Documents folder, visible in Finder/file explorer.
|
|
||||||
|
|
||||||
## Proposed Solution
|
|
||||||
|
|
||||||
Add a **Write tool step in SKILL.md** after the synthesis/stats/invitation block that saves the complete briefing to `~/Documents/Last30Days/{topic-slug}.md`. This is a SKILL.md-only change (no Python script modifications needed) because the content to save is the assistant's synthesized output, which only exists in the SKILL.md flow.
|
|
||||||
|
|
||||||
### Why SKILL.md, not the Python script
|
|
||||||
|
|
||||||
The Python script (`last30days.py`) runs first and produces raw scored items. The assistant then synthesizes these into the "What I learned" briefing, stats block, and invitation. The synthesis is the valuable part - it's what @devin_explores is saving. The script can't produce this because it runs before synthesis happens.
|
|
||||||
|
|
||||||
### File naming
|
|
||||||
|
|
||||||
Convert the TOPIC variable to a kebab-case slug for the filename:
|
|
||||||
- "Claude Code best practices" -> `claude-code-best-practices.md`
|
|
||||||
- "best rap songs 2026" -> `best-rap-songs-2026.md`
|
|
||||||
- "nano banana 2 prompting guide" -> `nano-banana-2-prompting-guide.md`
|
|
||||||
|
|
||||||
This matches the screenshot pattern exactly (e.g., `anthropic-claude-code-best-practices.md`, `seedance-video-prompting-guide.md`).
|
|
||||||
|
|
||||||
If a file with the same slug already exists, append a date suffix: `claude-code-best-practices-2026-03-05.md`. This handles re-researching the same topic without overwriting previous results.
|
|
||||||
|
|
||||||
### File content
|
|
||||||
|
|
||||||
The saved .md file should contain the complete research output in this order:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# {TOPIC}
|
|
||||||
|
|
||||||
> Researched {date} | Query type: {QUERY_TYPE} | Target tool: {TARGET_TOOL or "general"}
|
|
||||||
|
|
||||||
## What I learned
|
|
||||||
|
|
||||||
{The full synthesis section - topics, patterns, citations}
|
|
||||||
|
|
||||||
## Stats
|
|
||||||
|
|
||||||
{The full stats box with source counts and engagement}
|
|
||||||
|
|
||||||
## Follow-up suggestions
|
|
||||||
|
|
||||||
{The 2-3 specific suggestions from the invitation block}
|
|
||||||
|
|
||||||
---
|
|
||||||
*Generated by [last30days](https://github.com/mvanhorn/last30days-skill) v2.9*
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementation location in SKILL.md
|
|
||||||
|
|
||||||
Insert a new section between the current "LAST - Invitation" display and the "WAIT FOR USER'S RESPONSE" section. The Write tool call happens silently - no user prompt, no opt-in. Just save and briefly confirm.
|
|
||||||
|
|
||||||
## Technical Considerations
|
|
||||||
|
|
||||||
- **Cross-platform paths**: `~/Documents/` exists on macOS and most Linux desktops. On systems where it doesn't exist, `mkdir -p` handles creation. Windows WSL users get it too.
|
|
||||||
- **Permissions**: The Write tool in Claude Code can write to `~/Documents/` without issues. No sandbox concerns since this is the user's own Documents folder.
|
|
||||||
- **Filename sanitization**: Strip special characters, collapse whitespace to hyphens, lowercase. Keep it simple - no need for a library, just basic string ops in the SKILL.md instructions.
|
|
||||||
- **File size**: Based on the screenshot (9-34 KB files), the synthesis output is well within reasonable bounds.
|
|
||||||
- **No opt-out flag needed initially**: This is the default behavior. If users complain, a `--no-save` flag can be added later. Start with always-on since the screenshot proves users want this.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [x] Running `/last30days {topic}` creates `~/Documents/Last30Days/{topic-slug}.md` automatically
|
|
||||||
- [x] File contains: title, date, query metadata, full synthesis, stats block, follow-up suggestions
|
|
||||||
- [x] Filename is kebab-case slug of the topic (e.g., `claude-code-skills-guide.md`)
|
|
||||||
- [x] Duplicate topics get a date suffix instead of overwriting
|
|
||||||
- [x] Directory `~/Documents/Last30Days/` is created automatically if it doesn't exist
|
|
||||||
- [x] A brief confirmation line appears after the stats (e.g., "Saved to ~/Documents/Last30Days/claude-code-skills-guide.md")
|
|
||||||
- [x] Agent mode (`--agent`) also saves the file
|
|
||||||
- [x] No changes to the Python script - this is purely a SKILL.md addition
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
### Step 1: Add save instructions to SKILL.md
|
|
||||||
|
|
||||||
Insert a new section after the invitation block (after line ~496, before "WAIT FOR USER'S RESPONSE" at line ~499):
|
|
||||||
|
|
||||||
**New section in `SKILL.md`:**
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Save Research to Documents
|
|
||||||
|
|
||||||
After displaying the invitation, save the complete research briefing:
|
|
||||||
|
|
||||||
1. Generate the filename from TOPIC:
|
|
||||||
- Lowercase the topic
|
|
||||||
- Replace spaces and special characters with hyphens
|
|
||||||
- Remove consecutive hyphens
|
|
||||||
- Trim to 60 characters max
|
|
||||||
- Example: "Claude Code Best Practices" -> "claude-code-best-practices"
|
|
||||||
|
|
||||||
2. Check if file already exists. If so, append today's date:
|
|
||||||
- "claude-code-best-practices.md" exists -> use "claude-code-best-practices-2026-03-05.md"
|
|
||||||
|
|
||||||
3. Use the Write tool to save to ~/Documents/Last30Days/{slug}.md with this content:
|
|
||||||
- H1 title: the TOPIC
|
|
||||||
- Metadata line: date, QUERY_TYPE, TARGET_TOOL
|
|
||||||
- Full "What I learned" synthesis (everything you just displayed)
|
|
||||||
- Full stats block
|
|
||||||
- Follow-up suggestions from the invitation
|
|
||||||
- Footer with skill attribution
|
|
||||||
|
|
||||||
4. Confirm briefly: "Saved to ~/Documents/Last30Days/{slug}.md"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Update agent mode section
|
|
||||||
|
|
||||||
The `--agent` mode section (line ~116) skips interactive elements but should still save. Add a note that agent mode saves the file with the same logic.
|
|
||||||
|
|
||||||
### Step 3: Update Security & Permissions section
|
|
||||||
|
|
||||||
Add to the "What this skill does" list (line ~599):
|
|
||||||
- "Saves research briefings as .md files to ~/Documents/Last30Days/"
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
|
|
||||||
- Users accumulate a browsable library of .md research files in their Documents folder
|
|
||||||
- No more manual copy-paste workflow to save results
|
|
||||||
- Files are immediately findable in Finder/file explorer search
|
|
||||||
|
|
||||||
## Dependencies & Risks
|
|
||||||
|
|
||||||
- **Low risk**: Write tool is already in the skill's `allowed-tools` list
|
|
||||||
- **Low risk**: ~/Documents/ is a standard, user-owned directory
|
|
||||||
- **Edge case**: If the skill is interrupted mid-run (before synthesis), no file is saved - this is correct behavior since there's nothing to save yet
|
|
||||||
- **Edge case**: Very long topics could produce unwieldy filenames - the 60-char truncation handles this
|
|
||||||
|
|
||||||
## Sources & References
|
|
||||||
|
|
||||||
- Screenshot from @devin_explores showing manual .md file library in ~/Documents/Last30Days/
|
|
||||||
- Current output pipeline: `scripts/lib/render.py:798` (`write_outputs()`) writes to `~/.local/share/last30days/out/`
|
|
||||||
- SKILL.md synthesis flow: lines 275-496 (internalize research -> show summary -> invitation)
|
|
||||||
- Existing `--emit` modes: `scripts/last30days.py:1700` (`output_result()`)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
---
|
|
||||||
title: "fix: Eliminate save-related output noise after research"
|
|
||||||
type: fix
|
|
||||||
status: active
|
|
||||||
date: 2026-03-06
|
|
||||||
---
|
|
||||||
|
|
||||||
# fix: Eliminate save-related output noise after research
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
|
|
||||||
After research completes, the auto-save feature adds 3-5 lines of unwanted output below the clean invitation block. Every approach tried so far has made things worse:
|
|
||||||
|
|
||||||
| Version | Approach | Lines added | Side effects |
|
|
||||||
|---------|----------|-------------|--------------|
|
|
||||||
| v2.9.1 | `Write` tool | ~5 | Shows "Wrote 43 lines to..." |
|
|
||||||
| v2.9.2 | `run_in_background: true` | ~5 | Background callback triggers 2-4 min cogitation, model hallucinates fake "Human:" messages and generates unsolicited multi-paragraph responses |
|
|
||||||
| v2.9.3 | Foreground `cat >` heredoc | ~3 | Shows `(No output)`, still triggers 2-4 min cogitation |
|
|
||||||
|
|
||||||
**Current v2.9.3 noise (3 lines):**
|
|
||||||
```
|
|
||||||
⏺ Bash(mkdir -p ~/Documents/Last30Days && cat > ...)
|
|
||||||
⎿ (No output)
|
|
||||||
✻ Churned for 2m 38s
|
|
||||||
```
|
|
||||||
|
|
||||||
The root cause is that ANY tool call after the invitation creates unavoidable Claude Code UI chrome, and the model cogitates for minutes regardless of foreground vs background.
|
|
||||||
|
|
||||||
## Proposed Solutions
|
|
||||||
|
|
||||||
### Option A: Remove auto-save entirely (Recommended)
|
|
||||||
|
|
||||||
Remove the entire "Save Research to Documents" section from SKILL.md. The research lives in the conversation. Zero extra tool calls, zero extra lines, zero cogitation.
|
|
||||||
|
|
||||||
**Changes to `SKILL.md`:**
|
|
||||||
- Delete the "Save Research to Documents" section (~45 lines)
|
|
||||||
- Update agent mode reference (line 126) - remove save mention
|
|
||||||
- Update security section (line 612) - change "Saves research briefings" to past tense or conditional
|
|
||||||
- Remove `📎` footer line from invitation format
|
|
||||||
- Simplify "WAIT FOR USER'S RESPONSE" section
|
|
||||||
|
|
||||||
**What users lose:** Auto-saved .md files in `~/Documents/Last30Days/`
|
|
||||||
**What users gain:** Clean output ending exactly at the invitation block
|
|
||||||
|
|
||||||
### Option B: Move save into Python script
|
|
||||||
|
|
||||||
Add `--save-dir` flag to `last30days.py`. The script saves its raw output during its existing Bash call (which already runs). Zero extra tool calls.
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
- `scripts/last30days.py` - add `--save-dir` argument, write raw output to file at end of execution
|
|
||||||
- `SKILL.md` - add `--save-dir=~/Documents/Last30Days` to the script invocation, remove save section
|
|
||||||
|
|
||||||
**What users lose:** Saved file contains raw research data, not Claude's synthesis
|
|
||||||
**What users gain:** Zero extra lines, data is still preserved
|
|
||||||
|
|
||||||
### Option C: Opt-in save via follow-up command
|
|
||||||
|
|
||||||
Remove auto-save. Add a note to the invitation: "Say 'save' to save this research." When user says "save", run the Bash heredoc then.
|
|
||||||
|
|
||||||
**Changes to `SKILL.md`:**
|
|
||||||
- Delete auto-save section
|
|
||||||
- Add "save" as a recognized intent in "WHEN USER RESPONDS" section
|
|
||||||
- Save logic only runs when explicitly requested
|
|
||||||
|
|
||||||
**What users lose:** Nothing - save is still available on demand
|
|
||||||
**What users gain:** Clean output by default, save when they want it
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [ ] Research output ends at the invitation block with zero tool calls after it
|
|
||||||
- [ ] No `(No output)` line visible after invitation
|
|
||||||
- [ ] No multi-minute cogitation after invitation
|
|
||||||
- [ ] No hallucinated fake user messages
|
|
||||||
- [ ] Version bumped to v2.9.4
|
|
||||||
- [ ] CHANGELOG updated
|
|
||||||
- [ ] Synced to ~/.claude, ~/.agents, ~/.codex via sync.sh
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
- Source: `/Users/mvanhorn/last30days-skill-private/SKILL.md`
|
|
||||||
- Deployed to: `~/.claude/skills/last30days/SKILL.md`
|
|
||||||
- Sync command: `bash scripts/sync.sh`
|
|
||||||
- Current version: v2.9.3
|
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# Partner Credits: Zero-Registration Free Tier for last30days Users
|
||||||
|
|
||||||
|
**Type:** Partnership proposal for ScrapeCreators
|
||||||
|
**Date:** 2026-03-05
|
||||||
|
**Status:** Draft proposal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Pitch
|
||||||
|
|
||||||
|
Every last30days user gets 100 free ScrapeCreators credits — Reddit, TikTok, Instagram — without ever visiting scrapecreators.com or creating an account. When they run out, the skill tells them where to upgrade. ScrapeCreators gets a distribution channel. last30days gets a killer default experience.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Right now, new last30days users hit a wall:
|
||||||
|
|
||||||
|
1. Install the skill (30 seconds)
|
||||||
|
2. Try `/last30days AI video tools`
|
||||||
|
3. Get told they need a `SCRAPECREATORS_API_KEY`
|
||||||
|
4. Have to go to scrapecreators.com, register, get a key, paste it into `.env`
|
||||||
|
5. Many never come back
|
||||||
|
|
||||||
|
The best Reddit experience requires a key. The friction kills adoption.
|
||||||
|
|
||||||
|
## The Proposal: Machine-Bound Partner Tokens
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
**ScrapeCreators side:**
|
||||||
|
1. Issue last30days a **partner ID** (e.g., `partner_last30days`)
|
||||||
|
2. Accept a new header: `X-Partner-Device: <device_hash>`
|
||||||
|
3. On first request per device hash: allocate 100 credits, no registration needed
|
||||||
|
4. Track usage: `(partner_id, device_hash) → credits_remaining`
|
||||||
|
5. When credits hit 0: return `402` with upgrade URL in response body
|
||||||
|
|
||||||
|
**last30days side:**
|
||||||
|
1. On first run, generate a **device fingerprint** and cache it locally
|
||||||
|
2. If user has no `SCRAPECREATORS_API_KEY`, send requests with partner headers instead
|
||||||
|
3. When 402 comes back, show a friendly "upgrade" message
|
||||||
|
|
||||||
|
That's it. No accounts, no OAuth, no registration flow.
|
||||||
|
|
||||||
|
### The Device Fingerprint
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hashlib, platform, uuid, os
|
||||||
|
|
||||||
|
def get_device_id():
|
||||||
|
"""Generate a stable, hard-to-forge device fingerprint."""
|
||||||
|
# Use the OS-level machine ID (persists across reinstalls on most systems)
|
||||||
|
machine_id = _get_machine_id()
|
||||||
|
|
||||||
|
# Salt with the partner ID so the hash is useless outside this context
|
||||||
|
raw = f"last30days:{machine_id}"
|
||||||
|
return hashlib.sha256(raw.encode()).hexdigest()
|
||||||
|
|
||||||
|
def _get_machine_id():
|
||||||
|
"""Get the OS hardware/machine ID."""
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
# macOS: IOPlatformUUID (hardware-bound, survives OS reinstall)
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(
|
||||||
|
["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if "IOPlatformUUID" in line:
|
||||||
|
return line.split('"')[-2]
|
||||||
|
|
||||||
|
elif platform.system() == "Linux":
|
||||||
|
# Linux: /etc/machine-id (set at install time)
|
||||||
|
try:
|
||||||
|
return open("/etc/machine-id").read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: MAC address + hostname (less stable but reasonable)
|
||||||
|
return f"{uuid.getnode()}:{platform.node()}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this works:**
|
||||||
|
- macOS `IOPlatformUUID` is hardware-bound — can't change it without a new motherboard
|
||||||
|
- Linux `/etc/machine-id` is set at OS install — persists across reboots
|
||||||
|
- Hashed with `last30days:` prefix so the raw ID is never sent to ScrapeCreators
|
||||||
|
- Cached locally in `~/.config/last30days/.device_id` after first generation
|
||||||
|
|
||||||
|
### API Request Format
|
||||||
|
|
||||||
|
```
|
||||||
|
# Without partner credits (existing flow — user has their own key)
|
||||||
|
GET /v1/reddit/search?query=AI+tools
|
||||||
|
x-api-key: sc_user_abc123
|
||||||
|
|
||||||
|
# With partner credits (new — no registration needed)
|
||||||
|
GET /v1/reddit/search?query=AI+tools
|
||||||
|
x-api-key: sc_partner_last30days
|
||||||
|
X-Partner-Device: a1b2c3d4e5f6... (sha256 hex)
|
||||||
|
```
|
||||||
|
|
||||||
|
ScrapeCreators treats `sc_partner_last30days` as a special key class:
|
||||||
|
- Requires `X-Partner-Device` header
|
||||||
|
- Credits tracked per device hash, not per API key
|
||||||
|
- Rate limited per device (e.g., 10 requests/minute)
|
||||||
|
- 100 credits per unique device, lifetime
|
||||||
|
|
||||||
|
### What Counts as a Credit
|
||||||
|
|
||||||
|
One API call = one credit. A typical `/last30days` run uses roughly:
|
||||||
|
- 2-4 Reddit searches (global + subreddit drilldowns)
|
||||||
|
- 1-2 TikTok searches + 2-3 transcript fetches
|
||||||
|
- 1-2 Instagram searches + 2-3 transcript fetches
|
||||||
|
|
||||||
|
So ~10-15 credits per run. 100 credits ≈ **7-10 full research runs** before upgrade.
|
||||||
|
|
||||||
|
That's enough to get hooked.
|
||||||
|
|
||||||
|
## Abuse Prevention
|
||||||
|
|
||||||
|
### What we're defending against
|
||||||
|
|
||||||
|
| Threat | Likelihood | Impact |
|
||||||
|
|--------|-----------|--------|
|
||||||
|
| User spoofs device ID to get infinite credits | Low | Medium |
|
||||||
|
| Script generates thousands of fake device IDs | Medium | High |
|
||||||
|
| User shares partner key for non-last30days use | Low | Low |
|
||||||
|
|
||||||
|
### Defenses (simplest first)
|
||||||
|
|
||||||
|
**1. Hardware-bound device ID (primary defense)**
|
||||||
|
- macOS IOPlatformUUID can't be changed without hardware swap
|
||||||
|
- Linux machine-id requires root to change and breaks other software
|
||||||
|
- Not a cookie or config file — it's the machine itself
|
||||||
|
|
||||||
|
**2. Rate limiting per device (ScrapeCreators side)**
|
||||||
|
- 10 requests/minute per device hash
|
||||||
|
- Prevents scripted rapid-fire abuse even with valid device IDs
|
||||||
|
- Normal usage never hits this — a full run takes 60-70 seconds with natural gaps
|
||||||
|
|
||||||
|
**3. IP rate limiting on new device registrations (ScrapeCreators side)**
|
||||||
|
- Max 3 new device hashes per IP per day
|
||||||
|
- Stops "generate 1000 device IDs from one server" attacks
|
||||||
|
- Legitimate users: one machine, one device ID, done
|
||||||
|
|
||||||
|
**4. Total partner pool cap (safety valve)**
|
||||||
|
- ScrapeCreators sets a monthly cap on total partner credits (e.g., 50,000/month)
|
||||||
|
- If last30days goes viral and blows the cap, both parties renegotiate
|
||||||
|
- Prevents runaway costs from unexpected growth
|
||||||
|
|
||||||
|
### What we're NOT doing (intentional simplicity)
|
||||||
|
|
||||||
|
- No CAPTCHAs
|
||||||
|
- No email verification
|
||||||
|
- No phone verification
|
||||||
|
- No browser fingerprinting
|
||||||
|
- No token signing or crypto
|
||||||
|
- No account creation whatsoever
|
||||||
|
|
||||||
|
The goal is zero friction. The device ID is "good enough" — it stops casual abuse and scripts. A determined attacker could maybe get 200-300 free credits by VM gymnastics, but that's not worth defending against when paid plans are cheap.
|
||||||
|
|
||||||
|
## User Experience
|
||||||
|
|
||||||
|
### First run (no key configured)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ /last30days AI video tools
|
||||||
|
|
||||||
|
🔍 Searching Reddit, TikTok, Instagram...
|
||||||
|
ℹ️ Using 100 free partner credits from ScrapeCreators (93 remaining)
|
||||||
|
Get your own key for unlimited use: scrapecreators.com/last30days
|
||||||
|
|
||||||
|
[... normal results ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Credits running low
|
||||||
|
|
||||||
|
```
|
||||||
|
ℹ️ 12 partner credits remaining. Get unlimited access: scrapecreators.com/last30days
|
||||||
|
```
|
||||||
|
|
||||||
|
### Credits exhausted
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ Free partner credits used up!
|
||||||
|
|
||||||
|
Reddit, TikTok, and Instagram require a ScrapeCreators API key.
|
||||||
|
Get one at: scrapecreators.com/last30days (100 free credits on signup, then pay-as-you-go)
|
||||||
|
|
||||||
|
Continuing with X, YouTube, Hacker News, Polymarket, and web search...
|
||||||
|
```
|
||||||
|
|
||||||
|
Key detail: the skill **doesn't stop working** — it gracefully falls back to the sources that don't need a key. The user still gets value, but they see what they're missing.
|
||||||
|
|
||||||
|
### After upgrade
|
||||||
|
|
||||||
|
```
|
||||||
|
$ echo 'SCRAPECREATORS_API_KEY=sc_abc123' >> ~/.config/last30days/.env
|
||||||
|
|
||||||
|
# Next run — partner headers no longer sent, user's own key used
|
||||||
|
```
|
||||||
|
|
||||||
|
## What ScrapeCreators Gets
|
||||||
|
|
||||||
|
1. **Distribution channel** — every last30days install is a potential paying customer
|
||||||
|
2. **Zero support burden** — no accounts to manage for free tier users
|
||||||
|
3. **Qualified leads** — users who exhaust 100 credits are proven power users
|
||||||
|
4. **Co-marketing** — "Powered by ScrapeCreators" in every skill run
|
||||||
|
5. **Usage data** — anonymous device-level usage patterns across topics
|
||||||
|
|
||||||
|
## What last30days Gets
|
||||||
|
|
||||||
|
1. **Zero-config Reddit** — install and go, no registration anywhere
|
||||||
|
2. **TikTok and Instagram included** — three sources work out of the box
|
||||||
|
3. **Lower barrier to adoption** — the #1 friction point eliminated
|
||||||
|
4. **Upgrade path built in** — natural conversion funnel
|
||||||
|
|
||||||
|
## Implementation Effort
|
||||||
|
|
||||||
|
### ScrapeCreators side (their work)
|
||||||
|
- [ ] Create partner key class with per-device credit tracking
|
||||||
|
- [ ] Accept `X-Partner-Device` header on partner keys
|
||||||
|
- [ ] Return `402` with upgrade URL when credits exhausted
|
||||||
|
- [ ] Rate limit: 10 req/min per device, 3 new devices/day per IP
|
||||||
|
- [ ] Dashboard for last30days to see aggregate partner usage
|
||||||
|
|
||||||
|
### last30days side (our work)
|
||||||
|
- [ ] `scripts/lib/device_id.py` — generate and cache device fingerprint (~30 lines)
|
||||||
|
- [ ] Update `scripts/lib/env.py` — fall back to partner auth when no user key
|
||||||
|
- [ ] Update `_sc_headers()` in reddit.py, tiktok.py, instagram.py — add partner headers
|
||||||
|
- [ ] Handle 402 response — show upgrade message, continue with other sources
|
||||||
|
- [ ] Show credits remaining in run output (from response header)
|
||||||
|
|
||||||
|
### Suggested response headers from ScrapeCreators
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Partner-Credits-Remaining: 87
|
||||||
|
X-Partner-Credits-Total: 100
|
||||||
|
X-Partner-Upgrade-URL: https://scrapecreators.com/last30days
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Credit pool negotiation** — what monthly cap works for ScrapeCreators?
|
||||||
|
2. **Referral tracking** — should `scrapecreators.com/last30days` give a signup bonus or revenue share?
|
||||||
|
3. **Credit count per endpoint** — should transcript fetches cost the same as searches?
|
||||||
|
4. **Expiration** — do unused partner credits expire (e.g., 90 days)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
One device ID. One partner key. One header. 100 free credits. Zero registration.
|
||||||
|
|
||||||
|
The entire abuse prevention is: your computer has a hardware ID that you can't easily change. That's it. Simple, clever, and good enough.
|
||||||
+4
-8
@@ -2,19 +2,15 @@ The AI world reinvents itself every month. This skill keeps you current.
|
|||||||
|
|
||||||
`/last30days` researches your topic across **Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web** from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations.
|
`/last30days` researches your topic across **Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web** from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations.
|
||||||
|
|
||||||
## What's New in v2.9.1
|
|
||||||
|
|
||||||
**Auto-save to ~/Documents/Last30Days/.** Every run now saves the complete research briefing - synthesis, stats, and follow-up suggestions - as a topic-named `.md` file to your Documents folder. Build a personal research library without lifting a finger. Inspired by [@devin_explores](https://x.com/devin_explores) who was already doing this manually.
|
|
||||||
|
|
||||||
## Three Headline Features in v2.9
|
## Three Headline Features in v2.9
|
||||||
|
|
||||||
**1. ScrapeCreators Reddit as default.** One `SCRAPECREATORS_API_KEY` now covers Reddit, TikTok, and Instagram - three sources, one key. No more `OPENAI_API_KEY` required for Reddit search. Faster, more reliable, and simpler to configure.
|
**1. ScrapeCreators Reddit as default.** One `SCRAPECREATORS_API_KEY` now covers Reddit, TikTok, and Instagram — three sources, one key. No more `OPENAI_API_KEY` required for Reddit search. Faster, more reliable, and simpler to configure.
|
||||||
|
|
||||||
**2. Smart subreddit discovery.** Relevance-weighted scoring replaces pure frequency count. Each candidate subreddit is scored by `frequency x recency x topic-word match`, and a `UTILITY_SUBS` blocklist filters noise subs like r/tipofmytongue. Search "Claude Code skills" and get r/ClaudeAI, r/ClaudeCode, r/openclaw - not generic programming subs.
|
**2. Smart subreddit discovery.** Relevance-weighted scoring replaces pure frequency count. Each candidate subreddit is scored by `frequency × recency × topic-word match`, and a `UTILITY_SUBS` blocklist filters noise subs like r/tipofmytongue. Search "Claude Code skills" and get r/ClaudeAI, r/ClaudeCode, r/openclaw — not generic programming subs.
|
||||||
|
|
||||||
**3. Top comments elevated.** The best comment on each Reddit thread now carries a 10% weight in engagement scoring and displays prominently with upvote counts. Reddit's value is in the comments - now the skill surfaces them.
|
**3. Top comments elevated.** The best comment on each Reddit thread now carries a 10% weight in engagement scoring and displays prominently with `💬` and upvote counts. Reddit's value is in the comments — now the skill surfaces them.
|
||||||
|
|
||||||
Plus: **Instagram Reels** (v2.8), **Polymarket prediction markets** (v2.5), **YouTube transcripts** (v2.1), **bundled X search** - no external CLI needed.
|
Plus: **Instagram Reels** (v2.8), **Polymarket prediction markets** (v2.5), **YouTube transcripts** (v2.1), **bundled X search** — no external CLI needed.
|
||||||
|
|
||||||
## Beta Test Results (v2.9)
|
## Beta Test Results (v2.9)
|
||||||
|
|
||||||
|
|||||||
@@ -1250,13 +1250,6 @@ def main():
|
|||||||
default=False,
|
default=False,
|
||||||
help="Skip native web search backends (Parallel/Brave/OpenRouter). Use when the assistant has its own WebSearch tool.",
|
help="Skip native web search backends (Parallel/Brave/OpenRouter). Use when the assistant has its own WebSearch tool.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--save-dir",
|
|
||||||
type=str,
|
|
||||||
default=None,
|
|
||||||
metavar="DIR",
|
|
||||||
help="Auto-save raw research output to DIR/{topic-slug}.md",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args.topic = " ".join(args.topic) if args.topic else None
|
args.topic = " ".join(args.topic) if args.topic else None
|
||||||
@@ -1611,20 +1604,6 @@ def main():
|
|||||||
# Output result
|
# Output result
|
||||||
output_result(report, args.emit, web_needed, args.topic, from_date, to_date, missing_keys, args.days, source_info)
|
output_result(report, args.emit, web_needed, args.topic, from_date, to_date, missing_keys, args.days, source_info)
|
||||||
|
|
||||||
# Auto-save raw research to file if --save-dir is set
|
|
||||||
if args.save_dir:
|
|
||||||
import re
|
|
||||||
save_dir = Path(args.save_dir).expanduser()
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
slug = re.sub(r'[^a-z0-9]+', '-', args.topic.lower()).strip('-')[:60]
|
|
||||||
save_path = save_dir / f"{slug}-raw.md"
|
|
||||||
if save_path.exists():
|
|
||||||
save_path = save_dir / f"{slug}-raw-{datetime.now().strftime('%Y-%m-%d')}.md"
|
|
||||||
content = render.render_compact(report, missing_keys=missing_keys)
|
|
||||||
content += "\n" + render.render_source_status(report, source_info)
|
|
||||||
save_path.write_text(content, encoding="utf-8")
|
|
||||||
print(f"📎 {save_path}", file=sys.stderr)
|
|
||||||
|
|
||||||
# Persist findings to SQLite if requested
|
# Persist findings to SQLite if requested
|
||||||
if args.store:
|
if args.store:
|
||||||
import store as store_mod
|
import store as store_mod
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
.tweet { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); white-space: pre-wrap; font-size: 15px; line-height: 1.5; }
|
||||||
|
button { margin-top: 16px; padding: 10px 20px; background: #1d9bf0; color: white; border: none; border-radius: 20px; font-size: 14px; cursor: pointer; }
|
||||||
|
button:hover { background: #1a8cd8; }
|
||||||
|
.copied { background: #00ba7c; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="tweet" id="tweet">/last30days v2.9 is out. Reddit just got a massive upgrade.
|
||||||
|
|
||||||
|
Top comments are now first-class. The best comment on each thread gets 10% scoring weight and shows up like:
|
||||||
|
|
||||||
|
r/ClaudeAI — "Anthropic Released 32 Page Detailed Guide on Building Claude Skills" [1,470pts, 112 comments]
|
||||||
|
💬 Top comment (567 pts): "Can't wait to have Claude read this and explain it to me."
|
||||||
|
|
||||||
|
Reddit's value was always in the comments. Now the skill actually reads them.
|
||||||
|
|
||||||
|
The other big change: smart subreddit discovery. It scores every candidate by frequency × recency × topic-word match and blocklists utility subs that used to pollute results.
|
||||||
|
|
||||||
|
Before: search "Kanye West" → r/AskReddit, r/OutOfTheLoop
|
||||||
|
Now: r/hiphopheads, r/Kanye, r/NFCWestMemeWar
|
||||||
|
|
||||||
|
Shoutout to @ScrapeCreators — one API key now covers Reddit, TikTok, and Instagram. Three sources, one key. Should be cheaper than using an OpenAI key too.</div>
|
||||||
|
<button onclick="copyTweet()">Copy to clipboard</button>
|
||||||
|
<script>
|
||||||
|
function copyTweet() {
|
||||||
|
const text = document.getElementById('tweet').textContent;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
const btn = document.querySelector('button');
|
||||||
|
btn.textContent = 'Copied!';
|
||||||
|
btn.classList.add('copied');
|
||||||
|
setTimeout(() => { btn.textContent = 'Copy to clipboard'; btn.classList.remove('copied'); }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user