chore(release): 0.5.0

This commit is contained in:
Peter Steinberger
2026-01-01 14:44:09 +01:00
parent c78be5d1ea
commit 6463b06674
10 changed files with 139 additions and 39 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
# Changelog
## 0.4.2 — Unreleased
## 0.5.0 — 2026-01-01
### Added
- `likes` command to list your liked tweets (thanks @swairshah).
@@ -15,6 +15,7 @@
- Cookie extraction timeout is now configurable (default 30s on macOS) via `--cookie-timeout` / `BIRD_COOKIE_TIMEOUT_MS` (thanks @tylerseymour).
- Search now paginates beyond 20 results when using `-n` (thanks @ryanh-ai).
- Library exports are now separated from the CLI entrypoint for easier embedding.
## 0.4.1 — 2025-12-31
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@steipete/bird",
"version": "0.4.1",
"version": "0.5.0",
"description": "CLI tool for tweeting and replying via Twitter/X GraphQL API",
"type": "module",
"main": "dist/index.js",
+7 -2
View File
@@ -4,8 +4,13 @@ import { type OperationName, QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twi
import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js';
import { normalizeQuoteDepth } from './twitter-client-utils.js';
export type Constructor<T = object> = new (...args: unknown[]) => T;
export type AbstractConstructor<T = object> = abstract new (...args: unknown[]) => T;
// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement.
export type Constructor<T = object> = new (...args: any[]) => T;
// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement.
export type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;
export type Mixin<TBase extends AbstractConstructor<TwitterClientBase>, TAdded> = abstract new (
...args: ConstructorParameters<TBase>
) => TwitterClientBase & TAdded;
export abstract class TwitterClientBase {
protected authToken: string;
+17 -4
View File
@@ -1,9 +1,20 @@
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
import { TWITTER_MEDIA_METADATA_URL, TWITTER_UPLOAD_URL } from './twitter-client-constants.js';
import type { UploadMediaResult } from './twitter-client-types.js';
export function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
return class extends Base {
export interface TwitterClientMediaMethods {
uploadMedia(input: { data: Uint8Array; mimeType: string; alt?: string }): Promise<UploadMediaResult>;
}
export function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(
Base: TBase,
): Mixin<TBase, TwitterClientMediaMethods> {
abstract class TwitterClientMedia extends Base {
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
constructor(...args: any[]) {
super(...args);
}
private mediaCategoryForMime(mimeType: string): string | null {
if (mimeType.startsWith('image/')) {
if (mimeType === 'image/gif') {
@@ -162,5 +173,7 @@ export function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
};
}
return TwitterClientMedia;
}
+18 -4
View File
@@ -1,10 +1,22 @@
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL, TWITTER_STATUS_UPDATE_URL } from './twitter-client-constants.js';
import { buildTweetCreateFeatures } from './twitter-client-features.js';
import type { CreateTweetResponse, TweetResult } from './twitter-client-types.js';
export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
return class extends Base {
export interface TwitterClientPostingMethods {
tweet(text: string, mediaIds?: string[]): Promise<TweetResult>;
reply(text: string, replyToTweetId: string, mediaIds?: string[]): Promise<TweetResult>;
}
export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>>(
Base: TBase,
): Mixin<TBase, TwitterClientPostingMethods> {
abstract class TwitterClientPosting extends Base {
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
constructor(...args: any[]) {
super(...args);
}
/**
* Post a new tweet
*/
@@ -269,5 +281,7 @@ export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>
error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`,
};
}
};
}
return TwitterClientPosting;
}
+17 -4
View File
@@ -1,11 +1,22 @@
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
import { TWITTER_API_BASE } from './twitter-client-constants.js';
import { buildSearchFeatures } from './twitter-client-features.js';
import type { SearchResult, TweetData } from './twitter-client-types.js';
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
return class extends Base {
export interface TwitterClientSearchMethods {
search(query: string, count?: number): Promise<SearchResult>;
}
export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>(
Base: TBase,
): Mixin<TBase, TwitterClientSearchMethods> {
abstract class TwitterClientSearch extends Base {
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
constructor(...args: any[]) {
super(...args);
}
/**
* Search for tweets matching a query
*/
@@ -156,5 +167,7 @@ export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>
return { success: true, tweets };
}
};
}
return TwitterClientSearch;
}
+23 -8
View File
@@ -1,11 +1,24 @@
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
import { TWITTER_API_BASE } from './twitter-client-constants.js';
import { buildBookmarksFeatures, buildLikesFeatures } from './twitter-client-features.js';
import type { SearchResult } from './twitter-client-types.js';
import type { GraphqlTweetResult, SearchResult } from './twitter-client-types.js';
import { parseTweetsFromInstructions } from './twitter-client-utils.js';
export function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
return class extends Base {
export interface TwitterClientTimelineMethods {
getBookmarks(count?: number): Promise<SearchResult>;
getLikes(count?: number): Promise<SearchResult>;
getBookmarkFolderTimeline(folderId: string, count?: number): Promise<SearchResult>;
}
export function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(
Base: TBase,
): Mixin<TBase, TwitterClientTimelineMethods> {
abstract class TwitterClientTimelines extends Base {
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
constructor(...args: any[]) {
super(...args);
}
private async getBookmarksQueryIds(): Promise<string[]> {
const primary = await this.getQueryId('Bookmarks');
return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw']));
@@ -74,7 +87,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
content?: {
itemContent?: {
tweet_results?: {
result?: unknown;
result?: GraphqlTweetResult;
};
};
};
@@ -180,7 +193,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
content?: {
itemContent?: {
tweet_results?: {
result?: unknown;
result?: GraphqlTweetResult;
};
};
};
@@ -283,7 +296,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
content?: {
itemContent?: {
tweet_results?: {
result?: unknown;
result?: GraphqlTweetResult;
};
};
};
@@ -333,5 +346,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
return { success: false, error: firstAttempt.error };
}
};
}
return TwitterClientTimelines;
}
+19 -4
View File
@@ -1,4 +1,4 @@
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
import { TWITTER_API_BASE } from './twitter-client-constants.js';
import { buildArticleFeatures, buildArticleFieldToggles, buildTweetDetailFeatures } from './twitter-client-features.js';
import type { GetTweetResult, GraphqlTweetResult, SearchResult } from './twitter-client-types.js';
@@ -10,8 +10,21 @@ import {
parseTweetsFromInstructions,
} from './twitter-client-utils.js';
export function withTweetDetails<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
return class extends Base {
export interface TwitterClientTweetDetailMethods {
getTweet(tweetId: string): Promise<GetTweetResult>;
getReplies(tweetId: string): Promise<SearchResult>;
getThread(tweetId: string): Promise<SearchResult>;
}
export function withTweetDetails<TBase extends AbstractConstructor<TwitterClientBase>>(
Base: TBase,
): Mixin<TBase, TwitterClientTweetDetailMethods> {
abstract class TwitterClientTweetDetails extends Base {
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
constructor(...args: any[]) {
super(...args);
}
private async fetchUserArticlePlainText(
userId: string,
tweetId: string,
@@ -318,5 +331,7 @@ export function withTweetDetails<TBase extends AbstractConstructor<TwitterClient
return { success: true, tweets: thread };
}
};
}
return TwitterClientTweetDetails;
}
+19 -4
View File
@@ -1,4 +1,4 @@
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
import {
SETTINGS_NAME_REGEX,
SETTINGS_SCREEN_NAME_REGEX,
@@ -9,8 +9,21 @@ import { buildFollowingFeatures } from './twitter-client-features.js';
import type { CurrentUserResult, FollowingResult } from './twitter-client-types.js';
import { parseUsersFromInstructions } from './twitter-client-utils.js';
export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
return class extends Base {
export interface TwitterClientUserMethods {
getCurrentUser(): Promise<CurrentUserResult>;
getFollowing(userId: string, count?: number): Promise<FollowingResult>;
getFollowers(userId: string, count?: number): Promise<FollowingResult>;
}
export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
Base: TBase,
): Mixin<TBase, TwitterClientUserMethods> {
abstract class TwitterClientUsers extends Base {
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
constructor(...args: any[]) {
super(...args);
}
private async getFollowingQueryIds(): Promise<string[]> {
const primary = await this.getQueryId('Following');
return Array.from(new Set([primary, 'BEkNpEt5pNETESoqMsTEGA']));
@@ -326,5 +339,7 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
return { success: false, error: firstAttempt.error };
}
};
}
return TwitterClientUsers;
}
+16 -7
View File
@@ -1,14 +1,23 @@
import type { AbstractConstructor } from './twitter-client-base.js';
import { TwitterClientBase } from './twitter-client-base.js';
import { withMedia } from './twitter-client-media.js';
import { withPosting } from './twitter-client-posting.js';
import { withSearch } from './twitter-client-search.js';
import { withTimelines } from './twitter-client-timelines.js';
import { withTweetDetails } from './twitter-client-tweet-detail.js';
import { withUsers } from './twitter-client-users.js';
import { type TwitterClientMediaMethods, withMedia } from './twitter-client-media.js';
import { type TwitterClientPostingMethods, withPosting } from './twitter-client-posting.js';
import { type TwitterClientSearchMethods, withSearch } from './twitter-client-search.js';
import { type TwitterClientTimelineMethods, withTimelines } from './twitter-client-timelines.js';
import { type TwitterClientTweetDetailMethods, withTweetDetails } from './twitter-client-tweet-detail.js';
import { type TwitterClientUserMethods, withUsers } from './twitter-client-users.js';
type TwitterClientInstance = TwitterClientBase &
TwitterClientMediaMethods &
TwitterClientPostingMethods &
TwitterClientSearchMethods &
TwitterClientTimelineMethods &
TwitterClientTweetDetailMethods &
TwitterClientUserMethods;
const MixedTwitterClient = withUsers(
withTimelines(withSearch(withTweetDetails(withPosting(withMedia(TwitterClientBase))))),
);
) as AbstractConstructor<TwitterClientInstance>;
export class TwitterClient extends MixedTwitterClient {}