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:
Yilin Jing
2026-01-17 14:49:42 +08:00
commit 77139dcec2
45 changed files with 2608 additions and 0 deletions
+26
View File
@@ -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
+176
View File
@@ -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
+116
View File
@@ -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
View File
@@ -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...'"
+124
View File
@@ -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}" ...
```
+75
View File
@@ -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
+12
View File
@@ -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
+43
View File
@@ -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()
+41
View File
@@ -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()
+27
View File
@@ -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()
+35
View File
@@ -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()
+184
View File
@@ -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}")
+56
View File
@@ -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()
+105
View File
@@ -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/
+23
View File
@@ -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()
+16
View File
@@ -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
+35
View File
@@ -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()
+32
View File
@@ -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()
+31
View File
@@ -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()
+31
View File
@@ -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()
+34
View File
@@ -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()
+53
View File
@@ -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()
+26
View File
@@ -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()
+41
View File
@@ -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()
+21
View File
@@ -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()
+32
View File
@@ -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()
+34
View File
@@ -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()
+27
View File
@@ -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()
+148
View File
@@ -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}")
+36
View File
@@ -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