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 # Changelog
## 0.4.2 — Unreleased ## 0.5.0 — 2026-01-01
### Added ### Added
- `likes` command to list your liked tweets (thanks @swairshah). - `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). - 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). - Search now paginates beyond 20 results when using `-n` (thanks @ryanh-ai).
- Library exports are now separated from the CLI entrypoint for easier embedding. - Library exports are now separated from the CLI entrypoint for easier embedding.
## 0.4.1 — 2025-12-31 ## 0.4.1 — 2025-12-31
### Added ### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@steipete/bird", "name": "@steipete/bird",
"version": "0.4.1", "version": "0.5.0",
"description": "CLI tool for tweeting and replying via Twitter/X GraphQL API", "description": "CLI tool for tweeting and replying via Twitter/X GraphQL API",
"type": "module", "type": "module",
"main": "dist/index.js", "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 type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js';
import { normalizeQuoteDepth } from './twitter-client-utils.js'; import { normalizeQuoteDepth } from './twitter-client-utils.js';
export type Constructor<T = object> = new (...args: unknown[]) => T; // biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement.
export type AbstractConstructor<T = object> = abstract new (...args: unknown[]) => T; 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 { export abstract class TwitterClientBase {
protected authToken: string; 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 { TWITTER_MEDIA_METADATA_URL, TWITTER_UPLOAD_URL } from './twitter-client-constants.js';
import type { UploadMediaResult } from './twitter-client-types.js'; import type { UploadMediaResult } from './twitter-client-types.js';
export function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) { export interface TwitterClientMediaMethods {
return class extends Base { 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 { private mediaCategoryForMime(mimeType: string): string | null {
if (mimeType.startsWith('image/')) { if (mimeType.startsWith('image/')) {
if (mimeType === 'image/gif') { 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 { 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 { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL, TWITTER_STATUS_UPDATE_URL } from './twitter-client-constants.js';
import { buildTweetCreateFeatures } from './twitter-client-features.js'; import { buildTweetCreateFeatures } from './twitter-client-features.js';
import type { CreateTweetResponse, TweetResult } from './twitter-client-types.js'; import type { CreateTweetResponse, TweetResult } from './twitter-client-types.js';
export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) { export interface TwitterClientPostingMethods {
return class extends Base { 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 * Post a new tweet
*/ */
@@ -269,5 +281,7 @@ export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>
error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`, 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 { TWITTER_API_BASE } from './twitter-client-constants.js';
import { buildSearchFeatures } from './twitter-client-features.js'; import { buildSearchFeatures } from './twitter-client-features.js';
import type { SearchResult, TweetData } from './twitter-client-types.js'; import type { SearchResult, TweetData } from './twitter-client-types.js';
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js'; import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) { export interface TwitterClientSearchMethods {
return class extends Base { 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 * Search for tweets matching a query
*/ */
@@ -156,5 +167,7 @@ export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>
return { success: true, tweets }; 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 { TWITTER_API_BASE } from './twitter-client-constants.js';
import { buildBookmarksFeatures, buildLikesFeatures } from './twitter-client-features.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'; import { parseTweetsFromInstructions } from './twitter-client-utils.js';
export function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) { export interface TwitterClientTimelineMethods {
return class extends Base { 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[]> { private async getBookmarksQueryIds(): Promise<string[]> {
const primary = await this.getQueryId('Bookmarks'); const primary = await this.getQueryId('Bookmarks');
return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw'])); return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw']));
@@ -74,7 +87,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
content?: { content?: {
itemContent?: { itemContent?: {
tweet_results?: { tweet_results?: {
result?: unknown; result?: GraphqlTweetResult;
}; };
}; };
}; };
@@ -180,7 +193,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
content?: { content?: {
itemContent?: { itemContent?: {
tweet_results?: { tweet_results?: {
result?: unknown; result?: GraphqlTweetResult;
}; };
}; };
}; };
@@ -283,7 +296,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
content?: { content?: {
itemContent?: { itemContent?: {
tweet_results?: { tweet_results?: {
result?: unknown; result?: GraphqlTweetResult;
}; };
}; };
}; };
@@ -333,5 +346,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
return { success: false, error: firstAttempt.error }; 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 { TWITTER_API_BASE } from './twitter-client-constants.js';
import { buildArticleFeatures, buildArticleFieldToggles, buildTweetDetailFeatures } from './twitter-client-features.js'; import { buildArticleFeatures, buildArticleFieldToggles, buildTweetDetailFeatures } from './twitter-client-features.js';
import type { GetTweetResult, GraphqlTweetResult, SearchResult } from './twitter-client-types.js'; import type { GetTweetResult, GraphqlTweetResult, SearchResult } from './twitter-client-types.js';
@@ -10,8 +10,21 @@ import {
parseTweetsFromInstructions, parseTweetsFromInstructions,
} from './twitter-client-utils.js'; } from './twitter-client-utils.js';
export function withTweetDetails<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) { export interface TwitterClientTweetDetailMethods {
return class extends Base { 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( private async fetchUserArticlePlainText(
userId: string, userId: string,
tweetId: string, tweetId: string,
@@ -318,5 +331,7 @@ export function withTweetDetails<TBase extends AbstractConstructor<TwitterClient
return { success: true, tweets: thread }; 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 { import {
SETTINGS_NAME_REGEX, SETTINGS_NAME_REGEX,
SETTINGS_SCREEN_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 type { CurrentUserResult, FollowingResult } from './twitter-client-types.js';
import { parseUsersFromInstructions } from './twitter-client-utils.js'; import { parseUsersFromInstructions } from './twitter-client-utils.js';
export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) { export interface TwitterClientUserMethods {
return class extends Base { 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[]> { private async getFollowingQueryIds(): Promise<string[]> {
const primary = await this.getQueryId('Following'); const primary = await this.getQueryId('Following');
return Array.from(new Set([primary, 'BEkNpEt5pNETESoqMsTEGA'])); 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 { 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 { TwitterClientBase } from './twitter-client-base.js';
import { withMedia } from './twitter-client-media.js'; import { type TwitterClientMediaMethods, withMedia } from './twitter-client-media.js';
import { withPosting } from './twitter-client-posting.js'; import { type TwitterClientPostingMethods, withPosting } from './twitter-client-posting.js';
import { withSearch } from './twitter-client-search.js'; import { type TwitterClientSearchMethods, withSearch } from './twitter-client-search.js';
import { withTimelines } from './twitter-client-timelines.js'; import { type TwitterClientTimelineMethods, withTimelines } from './twitter-client-timelines.js';
import { withTweetDetails } from './twitter-client-tweet-detail.js'; import { type TwitterClientTweetDetailMethods, withTweetDetails } from './twitter-client-tweet-detail.js';
import { withUsers } from './twitter-client-users.js'; import { type TwitterClientUserMethods, withUsers } from './twitter-client-users.js';
type TwitterClientInstance = TwitterClientBase &
TwitterClientMediaMethods &
TwitterClientPostingMethods &
TwitterClientSearchMethods &
TwitterClientTimelineMethods &
TwitterClientTweetDetailMethods &
TwitterClientUserMethods;
const MixedTwitterClient = withUsers( const MixedTwitterClient = withUsers(
withTimelines(withSearch(withTweetDetails(withPosting(withMedia(TwitterClientBase))))), withTimelines(withSearch(withTweetDetails(withPosting(withMedia(TwitterClientBase))))),
); ) as AbstractConstructor<TwitterClientInstance>;
export class TwitterClient extends MixedTwitterClient {} export class TwitterClient extends MixedTwitterClient {}