mirror of
https://github.com/ReScienceLab/opc-skills.git
synced 2026-09-14 18:03:27 +08:00
Initial commit: Add reddit, twitter, and domain-hunter skills
Skills collection for One Person Companies (OPC). - reddit: Search Reddit via public JSON API - twitter: Search Twitter/X via twitterapi.io - domain-hunter: Find domains and compare registrar prices - Install script with multi-tool support (Claude, Droid, Cursor, etc.)
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,116 @@
|
||||
# OPC Skills
|
||||
|
||||
Agent Skills for One Person Companies (OPC) - a collection of skills designed for solopreneurs, indie hackers, and small teams.
|
||||
|
||||
## What are Skills?
|
||||
|
||||
Skills are folders of instructions, scripts, and resources that AI agents load dynamically to improve performance on specialized tasks. Each skill is self-contained with a `SKILL.md` file containing instructions and metadata.
|
||||
|
||||
For more information about the Agent Skills standard, see [agentskills.io](http://agentskills.io).
|
||||
|
||||
## Included Skills
|
||||
|
||||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
| [reddit](./skills/reddit) | Search and retrieve content from Reddit via the public JSON API |
|
||||
| [twitter](./skills/twitter) | Search and retrieve content from Twitter/X via twitterapi.io |
|
||||
| [domain-hunter](./skills/domain-hunter) | Find domains, compare registrar prices, and discover promo codes |
|
||||
|
||||
## Quick Install
|
||||
|
||||
### Using the Install Script (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/ReScienceLab/opc-skills.git
|
||||
cd opc-skills
|
||||
|
||||
# Interactive install
|
||||
./install.sh
|
||||
|
||||
# Or specify tool and skill directly
|
||||
./install.sh -t claude all # All skills to Claude Code
|
||||
./install.sh -t droid twitter # Twitter skill to Factory Droid
|
||||
./install.sh -t cursor -p reddit # Reddit to current project (Cursor)
|
||||
./install.sh -t custom -d ~/.my-agent/skills all
|
||||
```
|
||||
|
||||
### One-liner Install
|
||||
|
||||
```bash
|
||||
# Install all skills to Claude Code (global)
|
||||
curl -fsSL https://raw.githubusercontent.com/ReScienceLab/opc-skills/main/install.sh | bash -s -- -t claude all
|
||||
|
||||
# Install specific skill to Factory Droid
|
||||
curl -fsSL https://raw.githubusercontent.com/ReScienceLab/opc-skills/main/install.sh | bash -s -- -t droid reddit
|
||||
```
|
||||
|
||||
## Manual Installation
|
||||
|
||||
### Skill Directory Locations
|
||||
|
||||
| Tool | Global Directory | Project Directory |
|
||||
|------|------------------|-------------------|
|
||||
| Claude Code | `~/.claude/skills/` | `.claude/skills/` |
|
||||
| Factory Droid | `~/.factory/skills/` | `.factory/skills/` |
|
||||
| Cursor | - | `.cursor/skills/` |
|
||||
| OpenCode | `~/.config/opencode/skills/` | - |
|
||||
| Codex | `~/.codex/skills/` | `.codex/skills/` |
|
||||
|
||||
### Manual Copy
|
||||
|
||||
```bash
|
||||
# Clone repo
|
||||
git clone https://github.com/ReScienceLab/opc-skills.git
|
||||
|
||||
# Copy to your tool's skills directory
|
||||
# Example for Claude Code (global):
|
||||
mkdir -p ~/.claude/skills
|
||||
cp -r opc-skills/skills/reddit ~/.claude/skills/
|
||||
cp -r opc-skills/skills/twitter ~/.claude/skills/
|
||||
cp -r opc-skills/skills/domain-hunter ~/.claude/skills/
|
||||
|
||||
# Example for Factory Droid (project):
|
||||
mkdir -p .factory/skills
|
||||
cp -r opc-skills/skills/reddit .factory/skills/
|
||||
```
|
||||
|
||||
### Claude Code Plugin
|
||||
|
||||
```bash
|
||||
/plugin marketplace add ReScienceLab/opc-skills
|
||||
```
|
||||
|
||||
### Claude.ai
|
||||
|
||||
Upload skill folders via [Claude.ai settings](https://support.claude.com/en/articles/12512180-using-skills-in-claude#h_a4222fa77b).
|
||||
|
||||
### Claude API
|
||||
|
||||
See the [Skills API Quickstart](https://docs.claude.com/en/api/skills-guide#creating-a-skill).
|
||||
|
||||
## Creating New Skills
|
||||
|
||||
Use the template in `./template/SKILL.md` as a starting point:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill-name
|
||||
description: A clear description of what this skill does
|
||||
---
|
||||
|
||||
# My Skill Name
|
||||
|
||||
[Instructions for the AI agent]
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork this repository
|
||||
2. Create a new skill folder in `skills/`
|
||||
3. Add a `SKILL.md` with proper frontmatter
|
||||
4. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
#!/bin/bash
|
||||
|
||||
# OPC Skills Installer
|
||||
# Install agent skills to Claude Code, Factory Droid, OpenCode, or custom directories
|
||||
|
||||
set -e
|
||||
|
||||
REPO_URL="https://github.com/ReScienceLab/opc-skills"
|
||||
SKILLS_DIR="skills"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_header() {
|
||||
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ OPC Skills Installer ║${NC}"
|
||||
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
# Available skills
|
||||
AVAILABLE_SKILLS=("reddit" "twitter" "domain-hunter" "all")
|
||||
|
||||
show_help() {
|
||||
echo "Usage: ./install.sh [OPTIONS] <skill>"
|
||||
echo ""
|
||||
echo "Skills:"
|
||||
echo " reddit Reddit content search via public JSON API"
|
||||
echo " twitter Twitter/X search via twitterapi.io"
|
||||
echo " domain-hunter Domain search and price comparison"
|
||||
echo " all Install all skills"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -t, --tool TOOL Target tool: claude, droid, opencode, cursor, custom"
|
||||
echo " -d, --dir DIR Custom skills directory (use with -t custom)"
|
||||
echo " -p, --project Install to current project instead of global"
|
||||
echo " -h, --help Show this help"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ./install.sh -t claude reddit # Install reddit skill to Claude Code"
|
||||
echo " ./install.sh -t droid all # Install all skills to Factory Droid"
|
||||
echo " ./install.sh -t custom -d ~/.my-agent/skills twitter"
|
||||
echo " ./install.sh -t claude -p all # Install to current project's .claude/skills"
|
||||
echo ""
|
||||
echo "Default directories:"
|
||||
echo " claude: ~/.claude/skills (global) or .claude/skills (project)"
|
||||
echo " droid: ~/.factory/skills (global) or .factory/skills (project)"
|
||||
echo " cursor: .cursor/skills (project only)"
|
||||
echo " opencode: ~/.config/opencode/skills"
|
||||
}
|
||||
|
||||
get_skills_dir() {
|
||||
local tool=$1
|
||||
local project=$2
|
||||
|
||||
case $tool in
|
||||
claude)
|
||||
if [ "$project" = "true" ]; then
|
||||
echo ".claude/skills"
|
||||
else
|
||||
echo "$HOME/.claude/skills"
|
||||
fi
|
||||
;;
|
||||
droid)
|
||||
if [ "$project" = "true" ]; then
|
||||
echo ".factory/skills"
|
||||
else
|
||||
echo "$HOME/.factory/skills"
|
||||
fi
|
||||
;;
|
||||
cursor)
|
||||
echo ".cursor/skills"
|
||||
;;
|
||||
opencode)
|
||||
echo "$HOME/.config/opencode/skills"
|
||||
;;
|
||||
codex)
|
||||
if [ "$project" = "true" ]; then
|
||||
echo ".codex/skills"
|
||||
else
|
||||
echo "$HOME/.codex/skills"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_skill() {
|
||||
local skill=$1
|
||||
local target_dir=$2
|
||||
local source_dir=$3
|
||||
|
||||
if [ ! -d "$source_dir/$skill" ]; then
|
||||
print_error "Skill '$skill' not found in $source_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
if [ -d "$target_dir/$skill" ]; then
|
||||
print_warning "Skill '$skill' already exists, updating..."
|
||||
rm -rf "$target_dir/$skill"
|
||||
fi
|
||||
|
||||
cp -r "$source_dir/$skill" "$target_dir/"
|
||||
print_success "Installed $skill to $target_dir/$skill"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
TOOL=""
|
||||
CUSTOM_DIR=""
|
||||
PROJECT="false"
|
||||
SKILL=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--tool)
|
||||
TOOL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--dir)
|
||||
CUSTOM_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-p|--project)
|
||||
PROJECT="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
SKILL="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate inputs
|
||||
if [ -z "$SKILL" ]; then
|
||||
print_header
|
||||
echo "Select a skill to install:"
|
||||
echo ""
|
||||
echo " 1) reddit - Reddit content search"
|
||||
echo " 2) twitter - Twitter/X search"
|
||||
echo " 3) domain-hunter - Domain price comparison"
|
||||
echo " 4) all - All skills"
|
||||
echo ""
|
||||
read -p "Enter choice [1-4]: " choice
|
||||
case $choice in
|
||||
1) SKILL="reddit" ;;
|
||||
2) SKILL="twitter" ;;
|
||||
3) SKILL="domain-hunter" ;;
|
||||
4) SKILL="all" ;;
|
||||
*) print_error "Invalid choice"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ -z "$TOOL" ]; then
|
||||
echo ""
|
||||
echo "Select target tool:"
|
||||
echo ""
|
||||
echo " 1) claude - Claude Code"
|
||||
echo " 2) droid - Factory Droid"
|
||||
echo " 3) cursor - Cursor"
|
||||
echo " 4) opencode - OpenCode"
|
||||
echo " 5) codex - OpenAI Codex"
|
||||
echo " 6) custom - Custom directory"
|
||||
echo ""
|
||||
read -p "Enter choice [1-6]: " choice
|
||||
case $choice in
|
||||
1) TOOL="claude" ;;
|
||||
2) TOOL="droid" ;;
|
||||
3) TOOL="cursor" ;;
|
||||
4) TOOL="opencode" ;;
|
||||
5) TOOL="codex" ;;
|
||||
6) TOOL="custom" ;;
|
||||
*) print_error "Invalid choice"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Get target directory
|
||||
if [ "$TOOL" = "custom" ]; then
|
||||
if [ -z "$CUSTOM_DIR" ]; then
|
||||
read -p "Enter custom skills directory: " CUSTOM_DIR
|
||||
fi
|
||||
TARGET_DIR="$CUSTOM_DIR"
|
||||
else
|
||||
TARGET_DIR=$(get_skills_dir "$TOOL" "$PROJECT")
|
||||
fi
|
||||
|
||||
if [ -z "$TARGET_DIR" ]; then
|
||||
print_error "Unknown tool: $TOOL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find source directory (script location)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_DIR="$SCRIPT_DIR/$SKILLS_DIR"
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
print_error "Skills directory not found at $SOURCE_DIR"
|
||||
print_warning "Make sure you're running this from the opc-skills repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_header
|
||||
echo "Installing to: $TARGET_DIR"
|
||||
echo ""
|
||||
|
||||
# Install skill(s)
|
||||
if [ "$SKILL" = "all" ]; then
|
||||
for s in reddit twitter domain-hunter; do
|
||||
install_skill "$s" "$TARGET_DIR" "$SOURCE_DIR"
|
||||
done
|
||||
else
|
||||
install_skill "$SKILL" "$TARGET_DIR" "$SOURCE_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_success "Installation complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Restart your AI coding assistant"
|
||||
echo " 2. Try: 'Use the $SKILL skill to...'"
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: domain-hunter
|
||||
description: Search domains, compare prices, find promo codes, get purchase recommendations. Use when user wants to buy a domain, check prices, or find domain deals.
|
||||
triggers:
|
||||
- "domain"
|
||||
- "registrar"
|
||||
- "buy domain"
|
||||
- "domain price"
|
||||
- "promo code domain"
|
||||
- ".ai domain"
|
||||
- ".com domain"
|
||||
---
|
||||
|
||||
# Domain Hunter Skill
|
||||
|
||||
Help users find and purchase domain names at the best price.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Generate Domain Ideas & Check Availability
|
||||
|
||||
Based on the user's project description, generate 5-10 creative domain name suggestions.
|
||||
|
||||
**Guidelines:**
|
||||
- Keep names short (under 15 characters)
|
||||
- Make them memorable and brandable
|
||||
- Consider: `{action}{noun}`, `{noun}{suffix}`, `{prefix}{keyword}`
|
||||
- Common suffixes: app, io, hq, ly, ify, now, hub
|
||||
|
||||
**CRITICAL: Always check availability before presenting domains to user!**
|
||||
|
||||
Use one of these methods to verify availability:
|
||||
|
||||
**Method 1: WHOIS check (most reliable)**
|
||||
```bash
|
||||
# Check if domain is available via whois
|
||||
whois {domain}.{tld} 2>/dev/null | grep -i "no match\|not found\|available\|no data found" && echo "AVAILABLE" || echo "TAKEN"
|
||||
```
|
||||
|
||||
**Method 2: Registrar search page**
|
||||
Open the registrar's domain search in browser to verify:
|
||||
```bash
|
||||
open "https://www.spaceship.com/domains/?search={domain}.{tld}"
|
||||
```
|
||||
|
||||
**Method 3: Bulk check via Namecheap/Dynadot**
|
||||
- https://www.namecheap.com/domains/registration/results/?domain={domain}
|
||||
- https://www.dynadot.com/domain/search?domain={domain}
|
||||
|
||||
**IMPORTANT:**
|
||||
- Only present domains that are confirmed AVAILABLE
|
||||
- Mark any uncertain domains with "(unverified)"
|
||||
- Present suggestions to user and **wait for confirmation** before proceeding
|
||||
- Ask user to pick their preferred options or provide feedback
|
||||
- Only move to Step 2 after user approves domain name(s)
|
||||
|
||||
### Step 2: Compare Prices
|
||||
|
||||
Use **WebSearch** to find current prices:
|
||||
|
||||
```
|
||||
WebSearch: "cheapest .{tld} domain registrar 2026 site:tld-list.com"
|
||||
WebSearch: ".{tld} domain price comparison tldes.com"
|
||||
```
|
||||
|
||||
**Key price comparison sites:**
|
||||
- tld-list.com/tld/{tld}
|
||||
- tldes.com/{tld}
|
||||
- domaintyper.com/{tld}-domain
|
||||
|
||||
### Step 3: Find Promo Codes
|
||||
|
||||
Use **Twitter skill** to search registrar accounts:
|
||||
|
||||
```bash
|
||||
cd <twitter_skill_directory>
|
||||
python3 scripts/search_tweets.py "from:{registrar} promo code" --type Latest --limit 15
|
||||
python3 scripts/search_tweets.py "{registrar} promo code coupon" --type Latest --limit 15
|
||||
```
|
||||
|
||||
Use **Reddit skill** to search domain communities:
|
||||
|
||||
```bash
|
||||
cd <reddit_skill_directory>
|
||||
python3 scripts/search_posts.py "{registrar} promo code" --limit 15
|
||||
python3 scripts/search_posts.py "{registrar} coupon discount" --subreddit Domains --limit 10
|
||||
```
|
||||
|
||||
**Major registrar Twitter handles:**
|
||||
- @spaceship, @Dynadot, @Namecheap, @Porkbun, @namesilo, @Cloudflare
|
||||
|
||||
### Step 4: Recommend
|
||||
|
||||
Present final recommendation in this format:
|
||||
|
||||
```
|
||||
## Recommendation
|
||||
|
||||
**Domain:** example.ai
|
||||
**Best Registrar:** Spaceship
|
||||
**Price:** $68.98/year (2-year minimum = $137.96)
|
||||
**Promo Code:** None available for .ai
|
||||
**Purchase Link:** https://www.spaceship.com/
|
||||
|
||||
### Price Comparison
|
||||
| Registrar | Year 1 | Renewal | 2-Year Total |
|
||||
|-----------|--------|---------|--------------|
|
||||
| Spaceship | $68.98 | $68.98 | $137.96 |
|
||||
| Cloudflare| $70.00 | $70.00 | $140.00 |
|
||||
| Porkbun | $71.40 | $72.40 | $143.80 |
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Premium TLDs** (.ai, .io) rarely have promo codes - wholesale costs are too high
|
||||
2. **.ai domains** require 2-year minimum registration
|
||||
3. **Cloudflare** offers at-cost pricing with no markup
|
||||
4. **Renewal prices** often differ from registration - always check both
|
||||
5. **WHOIS privacy** is free at most registrars (Cloudflare, Namecheap, Porkbun)
|
||||
|
||||
## References
|
||||
|
||||
- [references/registrars.md](./references/registrars.md) - Detailed registrar comparison
|
||||
- [references/spaceship-api.md](./references/spaceship-api.md) - Spaceship API for automated domain operations
|
||||
@@ -0,0 +1,59 @@
|
||||
# Domain Registrar Reference
|
||||
|
||||
## Recommended Registrars
|
||||
|
||||
| Registrar | Best For | Pros | Cons |
|
||||
|-----------|----------|------|------|
|
||||
| **Cloudflare** | At-cost pricing | No markup, same renewal price, free privacy | Must add zone first |
|
||||
| **Spaceship** | Cheapest prices | Often lowest .ai/.io prices, clean UI | Newer company |
|
||||
| **NameSilo** | Consistent pricing | Low renewal, bulk discounts | Basic UI |
|
||||
| **Porkbun** | Budget friendly | Good prices, fun branding, free SSL | Limited enterprise features |
|
||||
| **Namecheap** | Features | Free WhoisGuard year 1, good support | Renewal can be higher |
|
||||
| **Dynadot** | Domain auctions | Good aftermarket, bulk tools | Interface dated |
|
||||
| **GoDaddy** | Avoid | Name recognition | High renewal, upsells |
|
||||
|
||||
## Price Tiers by TLD
|
||||
|
||||
### Premium TLDs (rarely have promos)
|
||||
- **.ai** - $65-80/year (Anguilla ccTLD, 2-year min)
|
||||
- **.io** - $35-50/year (British Indian Ocean Territory)
|
||||
- **.co** - $25-35/year (Colombia)
|
||||
|
||||
### Standard TLDs
|
||||
- **.com** - $9-14/year
|
||||
- **.net** - $12-16/year
|
||||
- **.org** - $10-14/year
|
||||
|
||||
### Budget TLDs
|
||||
- **.xyz** - $1-10/year
|
||||
- **.club** - $1-5/year
|
||||
- **.site** - $1-5/year
|
||||
|
||||
## Registrar Purchase Links
|
||||
|
||||
| Registrar | URL |
|
||||
|-----------|-----|
|
||||
| Cloudflare | https://dash.cloudflare.com/?to=/:account/domains/register |
|
||||
| Spaceship | https://www.spaceship.com/ |
|
||||
| NameSilo | https://www.namesilo.com/ |
|
||||
| Porkbun | https://porkbun.com/ |
|
||||
| Namecheap | https://www.namecheap.com/ |
|
||||
| Dynadot | https://www.dynadot.com/ |
|
||||
|
||||
## Twitter Handles for Promo Search
|
||||
|
||||
```
|
||||
@spaceship
|
||||
@Dynadot
|
||||
@Namecheap
|
||||
@Porkbun
|
||||
@namesilo
|
||||
@Cloudflare
|
||||
@GoDaddy
|
||||
```
|
||||
|
||||
## Reddit Communities
|
||||
|
||||
- r/Domains - General domain discussion
|
||||
- r/webhosting - Hosting + domains
|
||||
- r/Entrepreneur - Sometimes domain deals shared
|
||||
@@ -0,0 +1,312 @@
|
||||
# Spaceship API Reference
|
||||
|
||||
API keys configured in `~/.zshrc`:
|
||||
- `SPACESHIP_API_KEY`
|
||||
- `SPACESHIP_API_SECRET`
|
||||
|
||||
Base URL: `https://spaceship.dev/api`
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests require these headers:
|
||||
```bash
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY"
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
|
||||
Load environment variables:
|
||||
```bash
|
||||
export SPACESHIP_API_KEY="$(grep SPACESHIP_API_KEY ~/.zshrc | cut -d'"' -f2)"
|
||||
export SPACESHIP_API_SECRET="$(grep SPACESHIP_API_SECRET ~/.zshrc | cut -d'"' -f2)"
|
||||
```
|
||||
|
||||
## Domains API
|
||||
|
||||
### List Domains
|
||||
```bash
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/domains?take=100&skip=0" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
|
||||
### Check Domain Availability (Batch)
|
||||
```bash
|
||||
curl -s -X POST "https://spaceship.dev/api/v1/domains/available" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"domains": ["example.com", "test.ai"]}'
|
||||
```
|
||||
Response: `result` = "available" | "taken" | "reserved"
|
||||
|
||||
### Check Single Domain Availability
|
||||
```bash
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/domains/{domain}/available" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
|
||||
### Get Domain Info
|
||||
```bash
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/domains/{domain}" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
|
||||
### Register Domain (Purchase)
|
||||
```bash
|
||||
curl -s -X POST "https://spaceship.dev/api/v1/domains/{domain}" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"autoRenew": true,
|
||||
"years": 1,
|
||||
"privacyProtection": {
|
||||
"level": "high",
|
||||
"userConsent": true
|
||||
},
|
||||
"contacts": {
|
||||
"registrant": "CONTACT_ID",
|
||||
"admin": "CONTACT_ID",
|
||||
"tech": "CONTACT_ID",
|
||||
"billing": "CONTACT_ID"
|
||||
}
|
||||
}'
|
||||
```
|
||||
**Note:** Returns 202 Accepted, poll async-operations for result
|
||||
|
||||
### Update Nameservers (Configure to Cloudflare)
|
||||
```bash
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/domains/{domain}/nameservers" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"provider": "custom",
|
||||
"hosts": ["ns1.cloudflare.com", "ns2.cloudflare.com"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Update Auto-Renewal
|
||||
```bash
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/domains/{domain}/autorenew" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"isEnabled": true}'
|
||||
```
|
||||
|
||||
### Renew Domain
|
||||
```bash
|
||||
curl -s -X POST "https://spaceship.dev/api/v1/domains/{domain}/renew" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"years": 1}'
|
||||
```
|
||||
|
||||
### Update Privacy Protection
|
||||
```bash
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/domains/{domain}/privacy" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"level": "high",
|
||||
"userConsent": true
|
||||
}'
|
||||
```
|
||||
Levels: "high" | "medium" | "none"
|
||||
|
||||
### Update Domain Contacts
|
||||
```bash
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/domains/{domain}/contacts" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"registrant": "CONTACT_ID",
|
||||
"admin": "CONTACT_ID",
|
||||
"tech": "CONTACT_ID",
|
||||
"billing": "CONTACT_ID"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get Auth Code (for Transfer Out)
|
||||
```bash
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/domains/{domain}/auth-code" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
|
||||
### Update Transfer Lock
|
||||
```bash
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/domains/{domain}/transfer-lock" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"isEnabled": true}'
|
||||
```
|
||||
|
||||
### Transfer In Domain
|
||||
```bash
|
||||
curl -s -X POST "https://spaceship.dev/api/v1/domains/{domain}/transfer" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"authCode": "AUTH_CODE_FROM_CURRENT_REGISTRAR",
|
||||
"autoRenew": true,
|
||||
"privacyProtection": {
|
||||
"level": "high",
|
||||
"userConsent": true
|
||||
},
|
||||
"contacts": {
|
||||
"registrant": "CONTACT_ID",
|
||||
"admin": "CONTACT_ID",
|
||||
"tech": "CONTACT_ID",
|
||||
"billing": "CONTACT_ID"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Contacts API
|
||||
|
||||
### Save Contact
|
||||
```bash
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/contacts/{contactId}" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"organization": "Company Inc",
|
||||
"email": "john@example.com",
|
||||
"phone": "+1.5551234567",
|
||||
"address1": "123 Main St",
|
||||
"city": "San Francisco",
|
||||
"state": "CA",
|
||||
"postalCode": "94102",
|
||||
"country": "US"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get Contact
|
||||
```bash
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/contacts/{contactId}" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
|
||||
## DNS Records API
|
||||
|
||||
### Get DNS Records
|
||||
```bash
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/dns-records/{domain}?take=100&skip=0" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
|
||||
### Save DNS Records
|
||||
```bash
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/dns-records/{domain}" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"items": [
|
||||
{"type": "A", "name": "@", "address": "1.2.3.4", "ttl": 3600},
|
||||
{"type": "CNAME", "name": "www", "target": "example.com", "ttl": 3600},
|
||||
{"type": "TXT", "name": "@", "content": "v=spf1 ...", "ttl": 3600},
|
||||
{"type": "MX", "name": "@", "mailHost": "mail.example.com", "priority": 10, "ttl": 3600}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### Delete DNS Records
|
||||
```bash
|
||||
curl -s -X DELETE "https://spaceship.dev/api/v1/dns-records/{domain}" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"items": [
|
||||
{"type": "A", "name": "@", "address": "1.2.3.4"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Async Operations
|
||||
|
||||
Domain registration, transfer, etc. are async operations, returning `spaceship-async-operationid` header.
|
||||
|
||||
### Get Operation Status
|
||||
```bash
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/async-operations/{operationId}" \
|
||||
-H "X-Api-Key: $SPACESHIP_API_KEY" \
|
||||
-H "X-Api-Secret: $SPACESHIP_API_SECRET"
|
||||
```
|
||||
Status: "pending" | "success" | "failed"
|
||||
|
||||
## API Permissions
|
||||
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| domains:read | Read domain information |
|
||||
| domains:write | Manage domain settings |
|
||||
| domains:transfer | Domain transfer in/out |
|
||||
| domains:billing | Domain purchase and renewal |
|
||||
| contacts:read | Read contacts |
|
||||
| contacts:write | Save contacts |
|
||||
| dnsrecords:read | Read DNS records |
|
||||
| dnsrecords:write | Write DNS records |
|
||||
| asyncoperations:read | Query async operations |
|
||||
|
||||
## Rate Limits
|
||||
|
||||
| Operation | Limit |
|
||||
|-----------|-------|
|
||||
| List domains | 300 req / 300s per user |
|
||||
| Check availability (batch) | 30 req / 30s per user |
|
||||
| Check availability (single) | 5 req / 300s per domain |
|
||||
| Get domain info | 5 req / 300s per domain |
|
||||
| Register domain | 30 req / 30s per user |
|
||||
| Update nameservers | 5 req / 300s per domain |
|
||||
| Async operations | 60 req / 300s per user |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### 1. Purchase Domain + Configure Cloudflare NS
|
||||
|
||||
```bash
|
||||
# 1. Check availability
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/domains/example.com/available" ...
|
||||
|
||||
# 2. Get existing contact ID from another domain
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/domains?take=1&skip=0" ... | jq '.items[0].contacts.registrant'
|
||||
|
||||
# 3. Register domain
|
||||
curl -s -X POST "https://spaceship.dev/api/v1/domains/example.com" ... -d '{...}'
|
||||
# Note the operationId from response header
|
||||
|
||||
# 4. Poll for completion
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/async-operations/{operationId}" ...
|
||||
|
||||
# 5. Update nameservers to Cloudflare
|
||||
curl -s -X PUT "https://spaceship.dev/api/v1/domains/example.com/nameservers" \
|
||||
... -d '{"provider": "custom", "hosts": ["ns1.cloudflare.com", "ns2.cloudflare.com"]}'
|
||||
```
|
||||
|
||||
### 2. Transfer Domain to Spaceship
|
||||
|
||||
```bash
|
||||
# 1. Get auth code from current registrar
|
||||
# 2. Unlock domain at current registrar
|
||||
# 3. Initiate transfer
|
||||
curl -s -X POST "https://spaceship.dev/api/v1/domains/example.com/transfer" ...
|
||||
|
||||
# 4. Poll async operation for status
|
||||
curl -s -X GET "https://spaceship.dev/api/v1/async-operations/{operationId}" ...
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: reddit
|
||||
description: Search and retrieve content from Reddit. Get posts, comments, subreddit info, and user profiles via the public JSON API.
|
||||
triggers:
|
||||
- "reddit"
|
||||
- "subreddit"
|
||||
- "r/"
|
||||
---
|
||||
|
||||
# Reddit Skill
|
||||
|
||||
Get posts, comments, subreddit info, and user profiles from Reddit via the public JSON API.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**No API key required!** Reddit's public JSON API works without authentication.
|
||||
|
||||
**Quick Check**:
|
||||
```bash
|
||||
cd <skill_directory>
|
||||
python3 scripts/get_posts.py python --limit 3
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
All commands run from the skill directory.
|
||||
|
||||
### Subreddit Posts
|
||||
```bash
|
||||
python3 scripts/get_posts.py python --limit 20 # Hot posts (default)
|
||||
python3 scripts/get_posts.py python --sort new --limit 20
|
||||
python3 scripts/get_posts.py python --sort top --time week
|
||||
python3 scripts/get_posts.py python --sort top --time all --limit 10
|
||||
```
|
||||
|
||||
### Search Posts
|
||||
```bash
|
||||
python3 scripts/search_posts.py "AI agent" --limit 20
|
||||
python3 scripts/search_posts.py "MCP server" --subreddit ClaudeAI --limit 10
|
||||
python3 scripts/search_posts.py "async python" --sort top --time year
|
||||
```
|
||||
|
||||
### Subreddit Info
|
||||
```bash
|
||||
python3 scripts/get_subreddit.py python
|
||||
python3 scripts/get_subreddit.py ClaudeAI
|
||||
```
|
||||
|
||||
### Post & Comments
|
||||
```bash
|
||||
python3 scripts/get_post.py abc123 # Get post by ID
|
||||
python3 scripts/get_post.py abc123 --comments 50 # With more comments
|
||||
```
|
||||
|
||||
### User Profile
|
||||
```bash
|
||||
python3 scripts/get_user.py spez
|
||||
python3 scripts/get_user.py spez --posts 10 # Include recent posts
|
||||
```
|
||||
|
||||
## Sort Options
|
||||
|
||||
| Sort | Description | Time Options |
|
||||
|------|-------------|--------------|
|
||||
| `hot` | Trending posts (default) | - |
|
||||
| `new` | Latest posts | - |
|
||||
| `top` | Highest voted | hour, day, week, month, year, all |
|
||||
| `rising` | Gaining traction | - |
|
||||
| `controversial` | Mixed votes | hour, day, week, month, year, all |
|
||||
|
||||
## API Info
|
||||
- **Method**: Public JSON API (no auth needed)
|
||||
- **Trick**: Append `.json` to any Reddit URL
|
||||
- **Rate Limit**: 100 requests/minute
|
||||
- **Docs**: https://www.reddit.com/dev/api
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reddit API - No credentials needed for public read-only access.
|
||||
Just need a proper User-Agent header.
|
||||
"""
|
||||
|
||||
USER_AGENT = "DailyTasks-Reddit-Skill/1.0 (by /u/droid-assistant)"
|
||||
|
||||
|
||||
def get_user_agent() -> str:
|
||||
"""Get User-Agent for Reddit API requests"""
|
||||
return USER_AGENT
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get a post with comments
|
||||
Usage: python3 scripts/get_post.py POST_ID --comments 20
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from reddit_api import api_get, clean_post, print_post, print_comments_list
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get Reddit post with comments")
|
||||
parser.add_argument("post_id", help="Post ID (e.g., abc123)")
|
||||
parser.add_argument("--comments", "-c", type=int, default=20, help="Max comments")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Reddit returns [post_listing, comments_listing]
|
||||
data = api_get(f"comments/{args.post_id}", {"limit": args.comments})
|
||||
|
||||
if not isinstance(data, list) or len(data) < 2:
|
||||
print(f"Post not found: {args.post_id}")
|
||||
return
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
# First element is post listing
|
||||
post_listing = data[0].get("data", {}).get("children", [])
|
||||
if post_listing:
|
||||
post = clean_post(post_listing[0])
|
||||
print_post(post)
|
||||
|
||||
# Second element is comments listing
|
||||
comments_listing = data[1].get("data", {}).get("children", [])
|
||||
if comments_listing:
|
||||
print(f"---")
|
||||
print_comments_list(comments_listing[:args.comments])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get posts from a subreddit
|
||||
Usage: python3 scripts/get_posts.py python --sort hot --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from reddit_api import api_get, print_posts_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get subreddit posts")
|
||||
parser.add_argument("subreddit", help="Subreddit name (without r/)")
|
||||
parser.add_argument("--sort", "-s", choices=["hot", "new", "top", "rising", "controversial"],
|
||||
default="hot", help="Sort method (default: hot)")
|
||||
parser.add_argument("--time", "-t", choices=["hour", "day", "week", "month", "year", "all"],
|
||||
help="Time filter for top/controversial")
|
||||
parser.add_argument("--limit", "-l", type=int, default=25, help="Max posts (max 100)")
|
||||
parser.add_argument("--after", "-a", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
path = f"r/{args.subreddit}/{args.sort}"
|
||||
params = {
|
||||
"limit": min(args.limit, 100),
|
||||
"after": args.after,
|
||||
}
|
||||
if args.time and args.sort in ["top", "controversial"]:
|
||||
params["t"] = args.time
|
||||
|
||||
data = api_get(path, params)
|
||||
listing = data.get("data", {})
|
||||
posts = listing.get("children", [])
|
||||
|
||||
label = f"r/{args.subreddit}/{args.sort}"
|
||||
if args.time:
|
||||
label += f"/{args.time}"
|
||||
print_posts_list(posts, label)
|
||||
print_pagination(listing)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get subreddit info
|
||||
Usage: python3 scripts/get_subreddit.py python
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from reddit_api import api_get, clean_subreddit, print_subreddit
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get subreddit info")
|
||||
parser.add_argument("subreddit", help="Subreddit name (without r/)")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get(f"r/{args.subreddit}/about")
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(data.get("data", data), indent=2))
|
||||
return
|
||||
|
||||
print_subreddit(clean_subreddit(data))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user profile
|
||||
Usage: python3 scripts/get_user.py spez --posts 10
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from reddit_api import api_get, clean_user, print_user, print_posts_list
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get Reddit user profile")
|
||||
parser.add_argument("username", help="Username (without u/)")
|
||||
parser.add_argument("--posts", "-p", type=int, default=0, help="Include N recent posts")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get(f"user/{args.username}/about")
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(data.get("data", data), indent=2))
|
||||
return
|
||||
|
||||
print_user(clean_user(data))
|
||||
|
||||
if args.posts > 0:
|
||||
posts_data = api_get(f"user/{args.username}/submitted", {"limit": args.posts})
|
||||
posts = posts_data.get("data", {}).get("children", [])
|
||||
if posts:
|
||||
print(f"---")
|
||||
print_posts_list(posts, "recent_posts")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reddit public JSON API wrapper
|
||||
No authentication required - just append .json to URLs
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
import sys
|
||||
from credential import get_user_agent
|
||||
|
||||
BASE_URL = "https://www.reddit.com"
|
||||
|
||||
|
||||
def api_get(path: str, params: dict = None) -> dict:
|
||||
"""Make GET request to Reddit JSON API"""
|
||||
url = f"{BASE_URL}/{path}.json"
|
||||
if params:
|
||||
params["raw_json"] = "1" # Avoid HTML entity encoding
|
||||
filtered = {k: v for k, v in params.items() if v is not None}
|
||||
if filtered:
|
||||
url += "?" + urllib.parse.urlencode(filtered)
|
||||
else:
|
||||
url += "?raw_json=1"
|
||||
|
||||
headers = {"User-Agent": get_user_agent()}
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 429:
|
||||
print("error: Rate limited. Wait a moment and try again.", file=sys.stderr)
|
||||
elif e.code == 404:
|
||||
print(f"error: Not found - {path}", file=sys.stderr)
|
||||
else:
|
||||
print(f"error: HTTP {e.code}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def format_count(n) -> str:
|
||||
"""Format numbers (1234567 -> 1.2M)"""
|
||||
if n is None:
|
||||
return "0"
|
||||
n = int(n)
|
||||
if n >= 1_000_000:
|
||||
return f"{n/1_000_000:.1f}M"
|
||||
if n >= 1_000:
|
||||
return f"{n/1_000:.1f}K"
|
||||
return str(n)
|
||||
|
||||
|
||||
def clean_post(p: dict) -> dict:
|
||||
"""Clean post object from Reddit's data structure"""
|
||||
data = p.get("data", p)
|
||||
return {
|
||||
"id": data.get("id"),
|
||||
"title": data.get("title"),
|
||||
"subreddit": data.get("subreddit"),
|
||||
"author": data.get("author"),
|
||||
"score": data.get("score"),
|
||||
"upvote_ratio": data.get("upvote_ratio"),
|
||||
"num_comments": data.get("num_comments"),
|
||||
"url": data.get("url"),
|
||||
"permalink": f"https://reddit.com{data.get('permalink', '')}",
|
||||
"selftext": (data.get("selftext") or "")[:500],
|
||||
"created_utc": data.get("created_utc"),
|
||||
"is_self": data.get("is_self"),
|
||||
"link_flair_text": data.get("link_flair_text"),
|
||||
}
|
||||
|
||||
|
||||
def clean_comment(c: dict) -> dict:
|
||||
"""Clean comment object"""
|
||||
data = c.get("data", c)
|
||||
return {
|
||||
"id": data.get("id"),
|
||||
"author": data.get("author"),
|
||||
"body": (data.get("body") or "")[:300],
|
||||
"score": data.get("score"),
|
||||
"created_utc": data.get("created_utc"),
|
||||
}
|
||||
|
||||
|
||||
def clean_subreddit(s: dict) -> dict:
|
||||
"""Clean subreddit object"""
|
||||
data = s.get("data", s)
|
||||
return {
|
||||
"name": data.get("display_name"),
|
||||
"title": data.get("title"),
|
||||
"description": (data.get("public_description") or "")[:200],
|
||||
"subscribers": data.get("subscribers"),
|
||||
"active_users": data.get("accounts_active"),
|
||||
"created_utc": data.get("created_utc"),
|
||||
"url": f"https://reddit.com/r/{data.get('display_name')}",
|
||||
"over18": data.get("over18"),
|
||||
}
|
||||
|
||||
|
||||
def clean_user(u: dict) -> dict:
|
||||
"""Clean user object"""
|
||||
data = u.get("data", u)
|
||||
return {
|
||||
"name": data.get("name"),
|
||||
"link_karma": data.get("link_karma"),
|
||||
"comment_karma": data.get("comment_karma"),
|
||||
"created_utc": data.get("created_utc"),
|
||||
"is_mod": data.get("is_mod"),
|
||||
"verified": data.get("verified"),
|
||||
}
|
||||
|
||||
|
||||
def print_post(p: dict):
|
||||
"""Print post in TOON format"""
|
||||
if not p:
|
||||
return
|
||||
print(f"id: {p.get('id', '')}")
|
||||
print(f"title: {p.get('title', '')}")
|
||||
print(f"subreddit: r/{p.get('subreddit', '')}")
|
||||
print(f"author: u/{p.get('author', '')}")
|
||||
print(f"score: {format_count(p.get('score'))} ({int((p.get('upvote_ratio') or 0) * 100)}% upvoted)")
|
||||
print(f"comments: {format_count(p.get('num_comments'))}")
|
||||
print(f"url: {p.get('permalink', '')}")
|
||||
if p.get('link_flair_text'):
|
||||
print(f"flair: {p['link_flair_text']}")
|
||||
if p.get('selftext'):
|
||||
print(f"---")
|
||||
print(f"text: {p['selftext']}")
|
||||
|
||||
|
||||
def print_subreddit(s: dict):
|
||||
"""Print subreddit in TOON format"""
|
||||
if not s:
|
||||
return
|
||||
print(f"name: r/{s.get('name', '')}")
|
||||
print(f"title: {s.get('title', '')}")
|
||||
print(f"subscribers: {format_count(s.get('subscribers'))}")
|
||||
print(f"active: {format_count(s.get('active_users'))} online")
|
||||
print(f"nsfw: {s.get('over18', False)}")
|
||||
print(f"url: {s.get('url', '')}")
|
||||
if s.get('description'):
|
||||
print(f"description: {s['description']}")
|
||||
|
||||
|
||||
def print_user(u: dict):
|
||||
"""Print user in TOON format"""
|
||||
if not u:
|
||||
return
|
||||
print(f"name: u/{u.get('name', '')}")
|
||||
print(f"link_karma: {format_count(u.get('link_karma'))}")
|
||||
print(f"comment_karma: {format_count(u.get('comment_karma'))}")
|
||||
print(f"verified: {u.get('verified', False)}")
|
||||
print(f"is_mod: {u.get('is_mod', False)}")
|
||||
|
||||
|
||||
def print_posts_list(posts: list, label: str = "posts"):
|
||||
"""Print list of posts"""
|
||||
cleaned = [clean_post(p) for p in posts if p]
|
||||
print(f"{label}[{len(cleaned)}]{{title,subreddit,score,comments}}:")
|
||||
for p in cleaned:
|
||||
title = (p['title'] or '')[:60]
|
||||
print(f" {title},r/{p['subreddit']},{format_count(p['score'])},{format_count(p['num_comments'])}")
|
||||
|
||||
|
||||
def print_comments_list(comments: list, label: str = "comments"):
|
||||
"""Print list of comments"""
|
||||
cleaned = [clean_comment(c) for c in comments if c.get("kind") == "t1"]
|
||||
print(f"{label}[{len(cleaned)}]{{author,body,score}}:")
|
||||
for c in cleaned:
|
||||
body = (c['body'] or '')[:60].replace('\n', ' ')
|
||||
print(f" u/{c['author']},{body},{c['score']}")
|
||||
|
||||
|
||||
def print_pagination(data: dict):
|
||||
"""Print pagination info"""
|
||||
after = data.get("after")
|
||||
if after:
|
||||
print(f"---")
|
||||
print(f"has_next_page: True")
|
||||
print(f"next_cursor: {after}")
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Search posts on Reddit
|
||||
Usage: python3 scripts/search_posts.py "AI agent" --subreddit ClaudeAI --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from reddit_api import api_get, print_posts_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Search Reddit posts")
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--subreddit", "-r", help="Limit to subreddit")
|
||||
parser.add_argument("--sort", "-s", choices=["relevance", "hot", "top", "new", "comments"],
|
||||
default="relevance", help="Sort method")
|
||||
parser.add_argument("--time", "-t", choices=["hour", "day", "week", "month", "year", "all"],
|
||||
default="all", help="Time filter")
|
||||
parser.add_argument("--limit", "-l", type=int, default=25, help="Max posts")
|
||||
parser.add_argument("--after", "-a", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.subreddit:
|
||||
path = f"r/{args.subreddit}/search"
|
||||
params = {
|
||||
"q": args.query,
|
||||
"restrict_sr": "1",
|
||||
"sort": args.sort,
|
||||
"t": args.time,
|
||||
"limit": min(args.limit, 100),
|
||||
"after": args.after,
|
||||
}
|
||||
else:
|
||||
path = "search"
|
||||
params = {
|
||||
"q": args.query,
|
||||
"sort": args.sort,
|
||||
"t": args.time,
|
||||
"limit": min(args.limit, 100),
|
||||
"after": args.after,
|
||||
}
|
||||
|
||||
data = api_get(path, params)
|
||||
listing = data.get("data", {})
|
||||
posts = listing.get("children", [])
|
||||
|
||||
label = f"search({args.query})"
|
||||
if args.subreddit:
|
||||
label = f"r/{args.subreddit}/search({args.query})"
|
||||
print(f"query: {args.query}")
|
||||
print(f"sort: {args.sort}, time: {args.time}")
|
||||
print_posts_list(posts, label)
|
||||
print_pagination(listing)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: twitter
|
||||
description: Search and retrieve content from Twitter/X. Get user info, tweets, replies, followers, communities, spaces, and trends via twitterapi.io.
|
||||
triggers:
|
||||
- "twitter"
|
||||
- "X"
|
||||
- "推特"
|
||||
- "tweet"
|
||||
- "推文"
|
||||
---
|
||||
|
||||
# Twitter/X Skill
|
||||
|
||||
Get user profiles, tweets, replies, followers/following, communities, spaces, and trends from Twitter/X via twitterapi.io.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Set API key in `~/.zshrc`:
|
||||
```bash
|
||||
export TWITTERAPI_API_KEY="your_api_key"
|
||||
```
|
||||
|
||||
**Quick Check**:
|
||||
```bash
|
||||
cd <skill_directory>
|
||||
python3 scripts/get_user_info.py elonmusk
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
All commands run from the skill directory.
|
||||
|
||||
### User Endpoints
|
||||
```bash
|
||||
python3 scripts/get_user_info.py USERNAME
|
||||
python3 scripts/get_user_about.py USERNAME
|
||||
python3 scripts/batch_get_users.py USER_ID1,USER_ID2
|
||||
python3 scripts/get_user_tweets.py USERNAME --limit 20
|
||||
python3 scripts/get_user_mentions.py USERNAME --limit 20
|
||||
python3 scripts/get_followers.py USERNAME --limit 100
|
||||
python3 scripts/get_following.py USERNAME --limit 100
|
||||
python3 scripts/get_verified_followers.py USERNAME --limit 20
|
||||
python3 scripts/check_relationship.py USER1 USER2
|
||||
python3 scripts/search_users.py "query" --limit 20
|
||||
```
|
||||
|
||||
### Tweet Endpoints
|
||||
```bash
|
||||
python3 scripts/get_tweet.py TWEET_ID [TWEET_ID2...]
|
||||
python3 scripts/search_tweets.py "query" --type Latest --limit 20
|
||||
python3 scripts/get_tweet_replies.py TWEET_ID --limit 20
|
||||
python3 scripts/get_tweet_quotes.py TWEET_ID --limit 20
|
||||
python3 scripts/get_tweet_retweeters.py TWEET_ID --limit 50
|
||||
python3 scripts/get_tweet_thread.py TWEET_ID
|
||||
python3 scripts/get_article.py TWEET_ID
|
||||
```
|
||||
|
||||
### List Endpoints
|
||||
```bash
|
||||
python3 scripts/get_list_followers.py LIST_ID --limit 20
|
||||
python3 scripts/get_list_members.py LIST_ID --limit 20
|
||||
```
|
||||
|
||||
### Community Endpoints
|
||||
```bash
|
||||
python3 scripts/get_community.py COMMUNITY_ID
|
||||
python3 scripts/get_community_members.py COMMUNITY_ID --limit 20
|
||||
python3 scripts/get_community_moderators.py COMMUNITY_ID
|
||||
python3 scripts/get_community_tweets.py COMMUNITY_ID --limit 20
|
||||
python3 scripts/search_community_tweets.py "query" --limit 20
|
||||
```
|
||||
|
||||
### Other Endpoints
|
||||
```bash
|
||||
python3 scripts/get_space.py SPACE_ID
|
||||
python3 scripts/get_trends.py --woeid 1 # Worldwide
|
||||
```
|
||||
|
||||
## Search Query Syntax
|
||||
|
||||
```bash
|
||||
# Basic search
|
||||
python3 scripts/search_tweets.py "AI agent"
|
||||
|
||||
# From specific user
|
||||
python3 scripts/search_tweets.py "from:elonmusk"
|
||||
|
||||
# Date range
|
||||
python3 scripts/search_tweets.py "AI since:2024-01-01 until:2024-12-31"
|
||||
|
||||
# Exclude retweets
|
||||
python3 scripts/search_tweets.py "AI -filter:retweets"
|
||||
|
||||
# With media
|
||||
python3 scripts/search_tweets.py "AI filter:media"
|
||||
|
||||
# Minimum engagement
|
||||
python3 scripts/search_tweets.py "AI min_faves:1000"
|
||||
```
|
||||
|
||||
## API: twitterapi.io
|
||||
- Base URL: https://api.twitterapi.io/twitter
|
||||
- Auth: X-API-Key header
|
||||
- Pricing: ~$0.15-0.18/1k requests
|
||||
- Docs: https://docs.twitterapi.io/
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Batch get user info by user IDs
|
||||
Usage: python3 scripts/batch_get_users.py USER_ID1,USER_ID2,USER_ID3
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Batch get Twitter users by IDs")
|
||||
parser.add_argument("user_ids", help="Comma-separated user IDs")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get("user/batch_info_by_ids", {"userIds": args.user_ids})
|
||||
users = data.get("users") or data.get("data") or []
|
||||
|
||||
print(f"total: {len(users)}")
|
||||
print_users_list(users)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check follow relationship between two users
|
||||
Usage: python3 scripts/check_relationship.py USER1 USER2
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Check follow relationship")
|
||||
parser.add_argument("source", help="Source username")
|
||||
parser.add_argument("target", help="Target username")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"source_user_name": args.source, "target_user_name": args.target}
|
||||
data = api_get("user/check_follow_relationship", params)
|
||||
result = data.get("data") or data
|
||||
|
||||
print(f"source: @{args.source}")
|
||||
print(f"target: @{args.target}")
|
||||
print(f"source_follows_target: {result.get('following', False)}")
|
||||
print(f"target_follows_source: {result.get('followed_by', False)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Twitter API credential management.
|
||||
Reads from environment: TWITTERAPI_API_KEY
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
def get_twitter_api_key() -> str | None:
|
||||
"""Get TwitterAPI.io API key"""
|
||||
return os.environ.get("TWITTERAPI_API_KEY")
|
||||
|
||||
|
||||
def has_twitter_key() -> bool:
|
||||
"""Check if API key is available"""
|
||||
return get_twitter_api_key() is not None
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get long-form article from tweet
|
||||
Usage: python3 scripts/get_article.py TWEET_ID
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from twitter_api import api_get
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get Twitter article")
|
||||
parser.add_argument("tweet_id", help="Tweet ID with article")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get("article", {"tweet_id": args.tweet_id})
|
||||
article = data.get("data") or data
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(article, indent=2))
|
||||
return
|
||||
|
||||
print(f"tweet_id: {args.tweet_id}")
|
||||
if article.get("title"):
|
||||
print(f"title: {article['title']}")
|
||||
if article.get("content"):
|
||||
print(f"---")
|
||||
print(article["content"][:2000])
|
||||
if len(article.get("content", "")) > 2000:
|
||||
print(f"... (truncated, {len(article['content'])} chars total)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get community info by ID
|
||||
Usage: python3 scripts/get_community.py COMMUNITY_ID
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from twitter_api import api_get, format_count
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get community info")
|
||||
parser.add_argument("community_id", help="Community ID")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get("community", {"communityId": args.community_id})
|
||||
community = data.get("data") or data
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(community, indent=2))
|
||||
return
|
||||
|
||||
print(f"id: {community.get('id', '')}")
|
||||
print(f"name: {community.get('name', '')}")
|
||||
print(f"description: {community.get('description', '')[:200]}")
|
||||
print(f"members: {format_count(community.get('member_count'))}")
|
||||
print(f"created: {community.get('created_at', '')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get community members
|
||||
Usage: python3 scripts/get_community_members.py COMMUNITY_ID --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get community members")
|
||||
parser.add_argument("community_id", help="Community ID")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max members")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"communityId": args.community_id, "cursor": args.cursor}
|
||||
data = api_get("community/members", params)
|
||||
users = (data.get("members") or data.get("users") or [])[:args.limit]
|
||||
|
||||
print(f"community_id: {args.community_id}")
|
||||
print_users_list(users, "members")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get community moderators
|
||||
Usage: python3 scripts/get_community_moderators.py COMMUNITY_ID
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get community moderators")
|
||||
parser.add_argument("community_id", help="Community ID")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"communityId": args.community_id, "cursor": args.cursor}
|
||||
data = api_get("community/moderators", params)
|
||||
users = data.get("moderators") or data.get("users") or []
|
||||
|
||||
print(f"community_id: {args.community_id}")
|
||||
print_users_list(users, "moderators")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get community tweets
|
||||
Usage: python3 scripts/get_community_tweets.py COMMUNITY_ID --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get community tweets")
|
||||
parser.add_argument("community_id", help="Community ID")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max tweets")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"communityId": args.community_id, "cursor": args.cursor}
|
||||
data = api_get("community/tweets", params)
|
||||
tweets = (data.get("tweets") or [])[:args.limit]
|
||||
|
||||
print(f"community_id: {args.community_id}")
|
||||
print_tweets_list(tweets)
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user's followers
|
||||
Usage: python3 scripts/get_followers.py USERNAME --limit 100
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get user's followers")
|
||||
parser.add_argument("username", help="Twitter username (without @)")
|
||||
parser.add_argument("--limit", "-l", type=int, default=100, help="Max followers (max 200/page)")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {
|
||||
"userName": args.username,
|
||||
"cursor": args.cursor,
|
||||
"pageSize": min(args.limit, 200),
|
||||
}
|
||||
data = api_get("user/followers", params)
|
||||
users = (data.get("followers") or data.get("users") or [])[:args.limit]
|
||||
|
||||
print(f"username: @{args.username}")
|
||||
print_users_list(users, "followers")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user's following
|
||||
Usage: python3 scripts/get_following.py USERNAME --limit 100
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get user's following")
|
||||
parser.add_argument("username", help="Twitter username (without @)")
|
||||
parser.add_argument("--limit", "-l", type=int, default=100, help="Max following (max 200/page)")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {
|
||||
"userName": args.username,
|
||||
"cursor": args.cursor,
|
||||
"pageSize": min(args.limit, 200),
|
||||
}
|
||||
data = api_get("user/followings", params)
|
||||
users = (data.get("followings") or data.get("users") or [])[:args.limit]
|
||||
|
||||
print(f"username: @{args.username}")
|
||||
print_users_list(users, "following")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get list followers
|
||||
Usage: python3 scripts/get_list_followers.py LIST_ID --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get list followers")
|
||||
parser.add_argument("list_id", help="List ID")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max followers")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"list_id": args.list_id, "cursor": args.cursor}
|
||||
data = api_get("list/followers", params)
|
||||
users = (data.get("followers") or data.get("users") or [])[:args.limit]
|
||||
|
||||
print(f"list_id: {args.list_id}")
|
||||
print_users_list(users, "followers")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get list members
|
||||
Usage: python3 scripts/get_list_members.py LIST_ID --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get list members")
|
||||
parser.add_argument("list_id", help="List ID")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max members")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"list_id": args.list_id, "cursor": args.cursor}
|
||||
data = api_get("list/members", params)
|
||||
users = (data.get("members") or data.get("users") or [])[:args.limit]
|
||||
|
||||
print(f"list_id: {args.list_id}")
|
||||
print_users_list(users, "members")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get Twitter Space details
|
||||
Usage: python3 scripts/get_space.py SPACE_ID
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from twitter_api import api_get, format_count
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get Twitter Space details")
|
||||
parser.add_argument("space_id", help="Space ID")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get("space", {"spaceId": args.space_id})
|
||||
space = data.get("data") or data
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(space, indent=2))
|
||||
return
|
||||
|
||||
print(f"id: {space.get('id', '')}")
|
||||
print(f"title: {space.get('title', '')}")
|
||||
print(f"state: {space.get('state', '')}")
|
||||
print(f"host: @{space.get('creator', {}).get('userName', '')}")
|
||||
print(f"participants: {format_count(space.get('participant_count'))}")
|
||||
print(f"created: {space.get('created_at', '')}")
|
||||
print(f"started: {space.get('started_at', '')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get Twitter trends by location
|
||||
Usage: python3 scripts/get_trends.py --woeid 1
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from twitter_api import api_get, format_count
|
||||
|
||||
|
||||
# Common WOEIDs
|
||||
WOEIDS = {
|
||||
"worldwide": 1,
|
||||
"usa": 23424977,
|
||||
"uk": 23424975,
|
||||
"japan": 23424856,
|
||||
"india": 23424848,
|
||||
"brazil": 23424768,
|
||||
"canada": 23424775,
|
||||
"australia": 23424748,
|
||||
"germany": 23424829,
|
||||
"france": 23424819,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get Twitter trends")
|
||||
parser.add_argument("--woeid", "-w", type=int, default=1,
|
||||
help="WOEID (1=Worldwide, 23424977=USA)")
|
||||
parser.add_argument("--location", "-l", choices=list(WOEIDS.keys()),
|
||||
help="Location name (alternative to woeid)")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
woeid = WOEIDS.get(args.location, args.woeid) if args.location else args.woeid
|
||||
data = api_get("trends", {"woeid": woeid})
|
||||
trends = data.get("trends") or data.get("data") or []
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(trends, indent=2))
|
||||
return
|
||||
|
||||
print(f"woeid: {woeid}")
|
||||
print(f"trends[{len(trends)}]{{name,posts}}:")
|
||||
for t in trends[:30]:
|
||||
trend = t.get("trend", t)
|
||||
name = trend.get("name", "")
|
||||
posts = trend.get("meta_description", "-")
|
||||
print(f" {name},{posts}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get tweets by IDs
|
||||
Usage: python3 scripts/get_tweet.py TWEET_ID [TWEET_ID2...]
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, clean_tweet, print_tweet
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get tweets by IDs")
|
||||
parser.add_argument("tweet_ids", nargs="+", help="Tweet ID(s)")
|
||||
args = parser.parse_args()
|
||||
|
||||
ids = ",".join(args.tweet_ids)
|
||||
data = api_get("tweets", {"tweet_ids": ids})
|
||||
tweets = data.get("tweets") or []
|
||||
|
||||
for i, tweet in enumerate(tweets):
|
||||
if i > 0:
|
||||
print("---")
|
||||
print_tweet(clean_tweet(tweet))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get quote tweets
|
||||
Usage: python3 scripts/get_tweet_quotes.py TWEET_ID --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get quote tweets")
|
||||
parser.add_argument("tweet_id", help="Tweet ID")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max quotes")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"tweetId": args.tweet_id, "cursor": args.cursor}
|
||||
data = api_get("tweet/quotes", params)
|
||||
tweets = (data.get("tweets") or data.get("quotes") or [])[:args.limit]
|
||||
|
||||
print(f"tweet_id: {args.tweet_id}")
|
||||
print_tweets_list(tweets, "quotes")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get tweet replies
|
||||
Usage: python3 scripts/get_tweet_replies.py TWEET_ID --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get tweet replies")
|
||||
parser.add_argument("tweet_id", help="Tweet ID")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max replies")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"tweetId": args.tweet_id, "cursor": args.cursor}
|
||||
data = api_get("tweet/replies", params)
|
||||
tweets = (data.get("tweets") or data.get("replies") or [])[:args.limit]
|
||||
|
||||
print(f"tweet_id: {args.tweet_id}")
|
||||
print_tweets_list(tweets, "replies")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get users who retweeted a tweet
|
||||
Usage: python3 scripts/get_tweet_retweeters.py TWEET_ID --limit 50
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get tweet retweeters")
|
||||
parser.add_argument("tweet_id", help="Tweet ID")
|
||||
parser.add_argument("--limit", "-l", type=int, default=50, help="Max retweeters")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"tweetId": args.tweet_id, "cursor": args.cursor}
|
||||
data = api_get("tweet/retweeters", params)
|
||||
users = (data.get("users") or data.get("retweeters") or [])[:args.limit]
|
||||
|
||||
print(f"tweet_id: {args.tweet_id}")
|
||||
print_users_list(users, "retweeters")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get tweet thread context
|
||||
Usage: python3 scripts/get_tweet_thread.py TWEET_ID
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get tweet thread context")
|
||||
parser.add_argument("tweet_id", help="Tweet ID")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"tweetId": args.tweet_id, "cursor": args.cursor}
|
||||
data = api_get("tweet/thread_context", params)
|
||||
tweets = data.get("replies") or data.get("tweets") or []
|
||||
|
||||
print(f"tweet_id: {args.tweet_id}")
|
||||
print_tweets_list(tweets, "thread")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user profile about (extended info)
|
||||
Usage: python3 scripts/get_user_about.py USERNAME
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from twitter_api import api_get
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get Twitter user about/profile")
|
||||
parser.add_argument("username", help="Twitter username (without @)")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get("user_about", {"userName": args.username})
|
||||
user = data.get("data") or data
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(user, indent=2))
|
||||
return
|
||||
|
||||
print(f"id: {user.get('id', '')}")
|
||||
print(f"username: @{user.get('userName', '')}")
|
||||
print(f"name: {user.get('name', '')}")
|
||||
print(f"verified: {user.get('isBlueVerified', False)}")
|
||||
print(f"protected: {user.get('protected', False)}")
|
||||
print(f"created: {user.get('createdAt', '')}")
|
||||
|
||||
about = user.get("about_profile", {})
|
||||
if about:
|
||||
if about.get("account_based_in"):
|
||||
print(f"based_in: {about['account_based_in']}")
|
||||
changes = about.get("username_changes", {})
|
||||
if changes.get("count"):
|
||||
print(f"username_changes: {changes['count']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user info by username
|
||||
Usage: python3 scripts/get_user_info.py USERNAME
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, clean_user, print_user
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get Twitter user info")
|
||||
parser.add_argument("username", help="Twitter username (without @)")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = api_get("user/info", {"userName": args.username})
|
||||
user = data.get("data") or data
|
||||
print_user(clean_user(user))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user's mentions
|
||||
Usage: python3 scripts/get_user_mentions.py USERNAME --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get user's mentions")
|
||||
parser.add_argument("username", help="Twitter username (without @)")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max mentions")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"userName": args.username, "cursor": args.cursor}
|
||||
data = api_get("user/mentions", params)
|
||||
tweets = (data.get("tweets") or [])[:args.limit]
|
||||
|
||||
print(f"username: @{args.username}")
|
||||
print_tweets_list(tweets, "mentions")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user's recent tweets
|
||||
Usage: python3 scripts/get_user_tweets.py USERNAME --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get user's tweets")
|
||||
parser.add_argument("username", help="Twitter username (without @)")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max tweets")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
parser.add_argument("--include-replies", action="store_true", help="Include replies")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {
|
||||
"userName": args.username,
|
||||
"cursor": args.cursor,
|
||||
"includeReplies": "true" if args.include_replies else "false",
|
||||
}
|
||||
data = api_get("user/last_tweets", params)
|
||||
tweets = (data.get("tweets") or [])[:args.limit]
|
||||
|
||||
print(f"username: @{args.username}")
|
||||
print_tweets_list(tweets)
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get user's verified followers only
|
||||
Usage: python3 scripts/get_verified_followers.py USERNAME --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get user's verified followers")
|
||||
parser.add_argument("username", help="Twitter username (without @)")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max results")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
# First get user_id from username
|
||||
user_data = api_get("user/info", {"userName": args.username})
|
||||
user_id = (user_data.get("data") or user_data).get("id")
|
||||
|
||||
params = {"user_id": user_id, "cursor": args.cursor}
|
||||
data = api_get("user/verifiedFollowers", params)
|
||||
users = (data.get("followers") or data.get("users") or [])[:args.limit]
|
||||
|
||||
print(f"username: @{args.username}")
|
||||
print_users_list(users, "verified_followers")
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Search tweets from all communities
|
||||
Usage: python3 scripts/search_community_tweets.py "query" --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Search community tweets")
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max tweets")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"query": args.query, "queryType": "Latest", "cursor": args.cursor}
|
||||
data = api_get("community/get_tweets_from_all_community", params)
|
||||
tweets = (data.get("tweets") or [])[:args.limit]
|
||||
|
||||
print(f"query: {args.query}")
|
||||
print_tweets_list(tweets)
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Advanced tweet search
|
||||
Usage: python3 scripts/search_tweets.py "query" --type Latest --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_tweets_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Search tweets")
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--type", "-t", choices=["Latest", "Top"], default="Latest",
|
||||
help="Query type (default: Latest)")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max results")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {
|
||||
"query": args.query,
|
||||
"queryType": args.type,
|
||||
"cursor": args.cursor,
|
||||
}
|
||||
data = api_get("tweet/advanced_search", params)
|
||||
tweets = (data.get("tweets") or [])[:args.limit]
|
||||
|
||||
print(f"query: {args.query}")
|
||||
print(f"type: {args.type}")
|
||||
print_tweets_list(tweets)
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Search users by keyword
|
||||
Usage: python3 scripts/search_users.py "AI researcher" --limit 20
|
||||
"""
|
||||
import argparse
|
||||
from twitter_api import api_get, print_users_list, print_pagination
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Search Twitter users")
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="Max results")
|
||||
parser.add_argument("--cursor", "-c", help="Pagination cursor")
|
||||
args = parser.parse_args()
|
||||
|
||||
params = {"query": args.query, "cursor": args.cursor}
|
||||
data = api_get("user/search", params)
|
||||
users = (data.get("users") or [])[:args.limit]
|
||||
|
||||
print(f"query: {args.query}")
|
||||
print_users_list(users)
|
||||
print_pagination(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Twitter API wrapper using twitterapi.io
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
import sys
|
||||
from credential import get_twitter_api_key
|
||||
|
||||
API_BASE = "https://api.twitterapi.io/twitter"
|
||||
|
||||
|
||||
def api_get(endpoint: str, params: dict = None) -> dict:
|
||||
"""Make GET request to twitterapi.io"""
|
||||
api_key = get_twitter_api_key()
|
||||
if not api_key:
|
||||
print("error: TWITTERAPI_API_KEY not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
url = f"{API_BASE}/{endpoint}"
|
||||
if params:
|
||||
filtered = {k: v for k, v in params.items() if v is not None}
|
||||
if filtered:
|
||||
url += "?" + urllib.parse.urlencode(filtered)
|
||||
|
||||
req = urllib.request.Request(url, headers={"X-API-Key": api_key})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode()
|
||||
print(f"error: HTTP {e.code} - {error_body}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def format_count(n) -> str:
|
||||
"""Format numbers (1234567 -> 1.2M)"""
|
||||
if n is None:
|
||||
return "0"
|
||||
n = int(n)
|
||||
if n >= 1_000_000_000:
|
||||
return f"{n/1_000_000_000:.1f}B"
|
||||
if n >= 1_000_000:
|
||||
return f"{n/1_000_000:.1f}M"
|
||||
if n >= 1_000:
|
||||
return f"{n/1_000:.1f}K"
|
||||
return str(n)
|
||||
|
||||
|
||||
def clean_user(u: dict) -> dict:
|
||||
"""Clean user object"""
|
||||
if not u:
|
||||
return None
|
||||
return {
|
||||
"id": u.get("id"),
|
||||
"username": u.get("userName"),
|
||||
"name": u.get("name"),
|
||||
"verified": u.get("isBlueVerified"),
|
||||
"followers": u.get("followers"),
|
||||
"following": u.get("following"),
|
||||
"tweets": u.get("statusesCount"),
|
||||
"description": (u.get("description") or "")[:150],
|
||||
"location": u.get("location"),
|
||||
"created": u.get("createdAt"),
|
||||
}
|
||||
|
||||
|
||||
def clean_tweet(t: dict) -> dict:
|
||||
"""Clean tweet object"""
|
||||
if not t:
|
||||
return None
|
||||
author = t.get("author", {})
|
||||
return {
|
||||
"id": t.get("id"),
|
||||
"text": t.get("text"),
|
||||
"author": author.get("userName") if author else None,
|
||||
"author_name": author.get("name") if author else None,
|
||||
"created": t.get("createdAt"),
|
||||
"retweets": t.get("retweetCount"),
|
||||
"likes": t.get("likeCount"),
|
||||
"replies": t.get("replyCount"),
|
||||
"quotes": t.get("quoteCount"),
|
||||
"views": t.get("viewCount"),
|
||||
"lang": t.get("lang"),
|
||||
"isReply": t.get("isReply"),
|
||||
}
|
||||
|
||||
|
||||
def print_user(u: dict):
|
||||
"""Print user in TOON format"""
|
||||
if not u:
|
||||
return
|
||||
print(f"id: {u.get('id', '')}")
|
||||
print(f"username: @{u.get('username', '')}")
|
||||
print(f"name: {u.get('name', '')}")
|
||||
print(f"verified: {u.get('verified', False)}")
|
||||
print(f"followers: {format_count(u.get('followers'))}")
|
||||
print(f"following: {format_count(u.get('following'))}")
|
||||
print(f"tweets: {format_count(u.get('tweets'))}")
|
||||
if u.get('location'):
|
||||
print(f"location: {u['location']}")
|
||||
if u.get('description'):
|
||||
print(f"bio: {u['description']}")
|
||||
if u.get('created'):
|
||||
print(f"created: {u['created']}")
|
||||
|
||||
|
||||
def print_tweet(t: dict):
|
||||
"""Print tweet in TOON format"""
|
||||
if not t:
|
||||
return
|
||||
print(f"id: {t.get('id', '')}")
|
||||
print(f"author: @{t.get('author', '')} ({t.get('author_name', '')})")
|
||||
text = (t.get('text') or '')[:280]
|
||||
print(f"text: {text}")
|
||||
print(f"stats: {format_count(t.get('retweets'))} RT | {format_count(t.get('likes'))} likes | {format_count(t.get('replies'))} replies | {format_count(t.get('views'))} views")
|
||||
print(f"created: {t.get('created', '')}")
|
||||
|
||||
|
||||
def print_users_list(users: list, label: str = "users"):
|
||||
"""Print list of users"""
|
||||
cleaned = [clean_user(u) for u in users if u]
|
||||
print(f"{label}[{len(cleaned)}]{{username,name,followers,verified}}:")
|
||||
for u in cleaned:
|
||||
print(f" @{u['username']},{u['name']},{format_count(u['followers'])},{u['verified']}")
|
||||
|
||||
|
||||
def print_tweets_list(tweets: list, label: str = "tweets"):
|
||||
"""Print list of tweets"""
|
||||
cleaned = [clean_tweet(t) for t in tweets if t]
|
||||
print(f"{label}[{len(cleaned)}]{{id,author,text,likes}}:")
|
||||
for t in cleaned:
|
||||
text = (t['text'] or '')[:60].replace('\n', ' ')
|
||||
print(f" {t['id']},@{t['author']},{text},{format_count(t['likes'])}")
|
||||
|
||||
|
||||
def print_pagination(data: dict):
|
||||
"""Print pagination info"""
|
||||
has_next = data.get("has_next_page", False)
|
||||
cursor = data.get("next_cursor", "")
|
||||
if has_next and cursor:
|
||||
print(f"---")
|
||||
print(f"has_next_page: {has_next}")
|
||||
print(f"next_cursor: {cursor}")
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: my-skill-name
|
||||
description: A clear description of what this skill does and when to use it
|
||||
triggers:
|
||||
- "keyword1"
|
||||
- "keyword2"
|
||||
---
|
||||
|
||||
# My Skill Name
|
||||
|
||||
Brief description of what this skill does.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
List any required API keys, dependencies, or setup steps.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Example command
|
||||
python3 scripts/example.py --arg value
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
- Example usage 1
|
||||
- Example usage 2
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Guideline 1
|
||||
- Guideline 2
|
||||
|
||||
## References
|
||||
|
||||
- Link to relevant documentation
|
||||
Reference in New Issue
Block a user