Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ xurl read https://x.com/user/status/1234567890
xurl search "golang"
xurl search "from:elonmusk" -n 20
xurl search "#buildinpublic lang:en" -n 15

# Search with pagination / incremental polling
xurl search "from:elonmusk" -n 100 --since-id 1234567890
xurl search "crypto" --start-time 2025-01-01T00:00:00Z -n 50
xurl search "AI" --next-token TOKEN_FROM_PREVIOUS_RESPONSE

# Override default fields / expansions
xurl search "golang" --tweet-fields created_at,public_metrics,author_id --user-fields username,name --expansions author_id
```

### User Info
Expand All @@ -160,9 +168,20 @@ xurl user @XDevelopers
xurl timeline
xurl timeline -n 25

# Timeline with pagination / filtering
xurl timeline --since-id 1234567890 -n 50
xurl timeline --exclude replies,retweets
xurl timeline --start-time 2025-01-01T00:00:00Z -n 100
xurl timeline --next-token TOKEN_FROM_PREVIOUS_RESPONSE

# Your mentions
xurl mentions
xurl mentions -n 20

# Mentions with incremental polling
xurl mentions --since-id 1234567890 -n 50
xurl mentions --start-time 2025-03-01T00:00:00Z
xurl mentions --next-token TOKEN_FROM_PREVIOUS_RESPONSE
```

### Engagement
Expand Down Expand Up @@ -241,6 +260,24 @@ xurl post "lol" --media-id MEDIA_ID

---

## Pagination & Polling Flags

The `search`, `timeline`, and `mentions` commands accept these optional flags for incremental polling and field customisation:

| Flag | Applies to | Description |
|---|---|---|
| `--since-id ID` | search, timeline, mentions | Only return results with a tweet ID greater than this (exclusive). Ideal for incremental polling. |
| `--until-id ID` | search, timeline, mentions | Only return results with a tweet ID less than this (exclusive). |
| `--start-time TIME` | search, timeline, mentions | Oldest UTC datetime, ISO 8601 format (`YYYY-MM-DDTHH:mm:ssZ`). |
| `--end-time TIME` | search, timeline, mentions | Newest UTC datetime, ISO 8601 format. |
| `--next-token TOKEN` | search, timeline, mentions | Pagination token from a previous response's `meta.next_token`. |
| `--tweet-fields FIELDS` | search, timeline, mentions | Comma-separated tweet fields (overrides defaults: `created_at,public_metrics,conversation_id,entities`). |
| `--user-fields FIELDS` | search, timeline, mentions | Comma-separated user fields (overrides defaults: `username,name,verified`). |
| `--expansions FIELDS` | search, timeline, mentions | Comma-separated expansions (overrides default: `author_id`). |
| `--exclude TYPES` | timeline only | Comma-separated exclusions: `replies`, `retweets`, or both. |

---

## Global Flags

These flags work on every command:
Expand Down
129 changes: 123 additions & 6 deletions api/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,22 @@ func ReadPost(client Client, postID string, opts RequestOptions) (json.RawMessag
return client.SendRequest(opts)
}

// PaginationOptions holds optional pagination and field-override flags
// shared by search, timeline, and mentions commands.
type PaginationOptions struct {
SinceID string
UntilID string
StartTime string
EndTime string
NextToken string
TweetFields string
UserFields string
Expansions string
Exclude string // timeline/mentions only: "replies,retweets"
}

// SearchPosts searches recent posts.
func SearchPosts(client Client, query string, maxResults int, opts RequestOptions) (json.RawMessage, error) {
func SearchPosts(client Client, query string, maxResults int, pag PaginationOptions, opts RequestOptions) (json.RawMessage, error) {
q := url.QueryEscape(query)

// X API enforces min 10 / max 100 for search
Expand All @@ -169,8 +183,41 @@ func SearchPosts(client Client, query string, maxResults int, opts RequestOption
maxResults = 100
}

// Defaults
tweetFields := "created_at,public_metrics,conversation_id,entities"
userFields := "username,name,verified"
expansions := "author_id"
if pag.TweetFields != "" {
tweetFields = pag.TweetFields
}
if pag.UserFields != "" {
userFields = pag.UserFields
}
if pag.Expansions != "" {
expansions = pag.Expansions
}

endpoint := fmt.Sprintf("/2/tweets/search/recent?query=%s&max_results=%d&tweet.fields=%s&expansions=%s&user.fields=%s",
q, maxResults, tweetFields, expansions, userFields)

if pag.SinceID != "" {
endpoint += "&since_id=" + pag.SinceID
}
if pag.UntilID != "" {
endpoint += "&until_id=" + pag.UntilID
}
if pag.StartTime != "" {
endpoint += "&start_time=" + url.QueryEscape(pag.StartTime)
}
if pag.EndTime != "" {
endpoint += "&end_time=" + url.QueryEscape(pag.EndTime)
}
if pag.NextToken != "" {
endpoint += "&next_token=" + pag.NextToken
}

opts.Method = "GET"
opts.Endpoint = fmt.Sprintf("/2/tweets/search/recent?query=%s&max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name,verified", q, maxResults)
opts.Endpoint = endpoint
opts.Data = ""

return client.SendRequest(opts)
Expand Down Expand Up @@ -207,18 +254,88 @@ func GetUserPosts(client Client, userID string, maxResults int, opts RequestOpti

// GetTimeline fetches the authenticated user's reverse‑chronological timeline.
// Route: GET /2/users/{id}/timelines/reverse_chronological
func GetTimeline(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) {
func GetTimeline(client Client, userID string, maxResults int, pag PaginationOptions, opts RequestOptions) (json.RawMessage, error) {
tweetFields := "created_at,public_metrics,conversation_id,entities"
userFields := "username,name"
expansions := "author_id"
if pag.TweetFields != "" {
tweetFields = pag.TweetFields
}
if pag.UserFields != "" {
userFields = pag.UserFields
}
if pag.Expansions != "" {
expansions = pag.Expansions
}

endpoint := fmt.Sprintf("/2/users/%s/timelines/reverse_chronological?max_results=%d&tweet.fields=%s&expansions=%s&user.fields=%s",
userID, maxResults, tweetFields, expansions, userFields)

if pag.SinceID != "" {
endpoint += "&since_id=" + pag.SinceID
}
if pag.UntilID != "" {
endpoint += "&until_id=" + pag.UntilID
}
if pag.StartTime != "" {
endpoint += "&start_time=" + url.QueryEscape(pag.StartTime)
}
if pag.EndTime != "" {
endpoint += "&end_time=" + url.QueryEscape(pag.EndTime)
}
if pag.NextToken != "" {
endpoint += "&pagination_token=" + pag.NextToken
}
if pag.Exclude != "" {
endpoint += "&exclude=" + pag.Exclude
}

opts.Method = "GET"
opts.Endpoint = fmt.Sprintf("/2/users/%s/timelines/reverse_chronological?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name", userID, maxResults)
opts.Endpoint = endpoint
opts.Data = ""

return client.SendRequest(opts)
}

// GetMentions fetches recent mentions for a user.
func GetMentions(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) {
func GetMentions(client Client, userID string, maxResults int, pag PaginationOptions, opts RequestOptions) (json.RawMessage, error) {
tweetFields := "created_at,public_metrics,conversation_id,entities"
userFields := "username,name"
expansions := "author_id"
if pag.TweetFields != "" {
tweetFields = pag.TweetFields
}
if pag.UserFields != "" {
userFields = pag.UserFields
}
if pag.Expansions != "" {
expansions = pag.Expansions
}

endpoint := fmt.Sprintf("/2/users/%s/mentions?max_results=%d&tweet.fields=%s&expansions=%s&user.fields=%s",
userID, maxResults, tweetFields, expansions, userFields)

if pag.SinceID != "" {
endpoint += "&since_id=" + pag.SinceID
}
if pag.UntilID != "" {
endpoint += "&until_id=" + pag.UntilID
}
if pag.StartTime != "" {
endpoint += "&start_time=" + url.QueryEscape(pag.StartTime)
}
if pag.EndTime != "" {
endpoint += "&end_time=" + url.QueryEscape(pag.EndTime)
}
if pag.NextToken != "" {
endpoint += "&pagination_token=" + pag.NextToken
}
if pag.Exclude != "" {
endpoint += "&exclude=" + pag.Exclude
}

opts.Method = "GET"
opts.Endpoint = fmt.Sprintf("/2/users/%s/mentions?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name", userID, maxResults)
opts.Endpoint = endpoint
opts.Data = ""

return client.SendRequest(opts)
Expand Down
2 changes: 1 addition & 1 deletion api/shortcuts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func TestSearchPosts(t *testing.T) {
defer server.Close()
client := shortcutClient(t, server)

resp, err := SearchPosts(client, "golang", 10, baseTestOpts())
resp, err := SearchPosts(client, "golang", 10, PaginationOptions{}, baseTestOpts())
require.NoError(t, err)

var result struct {
Expand Down
79 changes: 73 additions & 6 deletions cli/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ Examples:

func searchCmd(a *auth.Auth) *cobra.Command {
var maxResults int
var sinceID, untilID, startTime, endTime, nextToken string
var tweetFields, userFields, expansions string
cmd := &cobra.Command{
Use: `search "QUERY"`,
Short: "Search recent posts",
Expand All @@ -262,15 +264,35 @@ func searchCmd(a *auth.Auth) *cobra.Command {
Examples:
xurl search "golang"
xurl search "from:elonmusk" -n 20
xurl search "#buildinpublic" -n 15`,
xurl search "#buildinpublic" -n 15
xurl search "from:user1 OR from:user2" -n 100 --since-id 1234567890
xurl search "crypto" --start-time 2025-01-01T00:00:00Z -n 50`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
client := newClient(a)
opts := baseOpts(cmd)
printResult(api.SearchPosts(client, args[0], maxResults, opts))
pag := api.PaginationOptions{
SinceID: sinceID,
UntilID: untilID,
StartTime: startTime,
EndTime: endTime,
NextToken: nextToken,
TweetFields: tweetFields,
UserFields: userFields,
Expansions: expansions,
}
printResult(api.SearchPosts(client, args[0], maxResults, pag, opts))
},
}
cmd.Flags().IntVarP(&maxResults, "max-results", "n", 10, "Number of results (min 10, max 100)")
cmd.Flags().StringVar(&sinceID, "since-id", "", "Only return results with an ID greater than this")
cmd.Flags().StringVar(&untilID, "until-id", "", "Only return results with an ID less than this")
cmd.Flags().StringVar(&startTime, "start-time", "", "Oldest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
cmd.Flags().StringVar(&endTime, "end-time", "", "Newest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
cmd.Flags().StringVar(&nextToken, "next-token", "", "Pagination token for next page of results")
cmd.Flags().StringVar(&tweetFields, "tweet-fields", "", "Comma-separated tweet fields (overrides defaults)")
cmd.Flags().StringVar(&userFields, "user-fields", "", "Comma-separated user fields (overrides defaults)")
cmd.Flags().StringVar(&expansions, "expansions", "", "Comma-separated expansions (overrides defaults)")
addCommonFlags(cmd)
return cmd
}
Expand Down Expand Up @@ -324,14 +346,18 @@ Examples:

func timelineCmd(a *auth.Auth) *cobra.Command {
var maxResults int
var sinceID, untilID, startTime, endTime, nextToken string
var tweetFields, userFields, expansions, exclude string
cmd := &cobra.Command{
Use: "timeline",
Short: "Show your home timeline",
Long: `Fetch your reverse‑chronological home timeline.

Examples:
xurl timeline
xurl timeline -n 25`,
xurl timeline -n 25
xurl timeline --since-id 1234567890 -n 50
xurl timeline --exclude replies,retweets`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
client := newClient(a)
Expand All @@ -341,24 +367,47 @@ Examples:
fmt.Fprintf(os.Stderr, "\033[31mError: %v\033[0m\n", err)
os.Exit(1)
}
printResult(api.GetTimeline(client, userID, maxResults, opts))
pag := api.PaginationOptions{
SinceID: sinceID,
UntilID: untilID,
StartTime: startTime,
EndTime: endTime,
NextToken: nextToken,
TweetFields: tweetFields,
UserFields: userFields,
Expansions: expansions,
Exclude: exclude,
}
printResult(api.GetTimeline(client, userID, maxResults, pag, opts))
},
}
cmd.Flags().IntVarP(&maxResults, "max-results", "n", 10, "Number of results (1–100)")
cmd.Flags().StringVar(&sinceID, "since-id", "", "Only return results with an ID greater than this")
cmd.Flags().StringVar(&untilID, "until-id", "", "Only return results with an ID less than this")
cmd.Flags().StringVar(&startTime, "start-time", "", "Oldest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
cmd.Flags().StringVar(&endTime, "end-time", "", "Newest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
cmd.Flags().StringVar(&nextToken, "next-token", "", "Pagination token for next page of results")
cmd.Flags().StringVar(&tweetFields, "tweet-fields", "", "Comma-separated tweet fields (overrides defaults)")
cmd.Flags().StringVar(&userFields, "user-fields", "", "Comma-separated user fields (overrides defaults)")
cmd.Flags().StringVar(&expansions, "expansions", "", "Comma-separated expansions (overrides defaults)")
cmd.Flags().StringVar(&exclude, "exclude", "", "Comma-separated exclusions: replies,retweets")
addCommonFlags(cmd)
return cmd
}

func mentionsCmd(a *auth.Auth) *cobra.Command {
var maxResults int
var sinceID, untilID, startTime, endTime, nextToken string
var tweetFields, userFields, expansions string
cmd := &cobra.Command{
Use: "mentions",
Short: "Show your recent mentions",
Long: `Fetch posts that mention the authenticated user.

Examples:
xurl mentions
xurl mentions -n 25`,
xurl mentions -n 25
xurl mentions --since-id 1234567890`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
client := newClient(a)
Expand All @@ -368,10 +417,28 @@ Examples:
fmt.Fprintf(os.Stderr, "\033[31mError: %v\033[0m\n", err)
os.Exit(1)
}
printResult(api.GetMentions(client, userID, maxResults, opts))
pag := api.PaginationOptions{
SinceID: sinceID,
UntilID: untilID,
StartTime: startTime,
EndTime: endTime,
NextToken: nextToken,
TweetFields: tweetFields,
UserFields: userFields,
Expansions: expansions,
}
printResult(api.GetMentions(client, userID, maxResults, pag, opts))
},
}
cmd.Flags().IntVarP(&maxResults, "max-results", "n", 10, "Number of results (5–100)")
cmd.Flags().StringVar(&sinceID, "since-id", "", "Only return results with an ID greater than this")
cmd.Flags().StringVar(&untilID, "until-id", "", "Only return results with an ID less than this")
cmd.Flags().StringVar(&startTime, "start-time", "", "Oldest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
cmd.Flags().StringVar(&endTime, "end-time", "", "Newest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
cmd.Flags().StringVar(&nextToken, "next-token", "", "Pagination token for next page of results")
cmd.Flags().StringVar(&tweetFields, "tweet-fields", "", "Comma-separated tweet fields (overrides defaults)")
cmd.Flags().StringVar(&userFields, "user-fields", "", "Comma-separated user fields (overrides defaults)")
cmd.Flags().StringVar(&expansions, "expansions", "", "Comma-separated expansions (overrides defaults)")
addCommonFlags(cmd)
return cmd
}
Expand Down