diff --git a/README.md b/README.md index 089ce35..d89796d 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Eight tools, dot-notation names form a navigable tree (`post.*`, `channel.*`, `p | `linkedin` | Browser fallback | returns `needs_browser` + compose URL | | `medium` | Browser fallback | returns `needs_browser` + compose URL | | `twitter` / `x` | Browser fallback | returns `needs_browser` + compose URL | +| `twitter_getxapi` / `getxapi_twitter` | Auto (read+write) | `GETXAPI_API_KEY` (writes require `GETXAPI_ENABLE_ACTIONS=true`) | ## Example agent call @@ -185,7 +186,7 @@ Agent (Claude Code / n8n / Cursor / any MCP host) ▼ content-distribution-mcp (this package, stdio transport) │ no LLM calls — pure I/O - ├── adapters/ devto · hashnode · github-discussions · reddit · bluesky · browser + ├── adapters/ devto · hashnode · github-discussions · reddit · bluesky · getxapi-twitter · browser └── backends/ yaml (post log · profiles · schedule queue · subreddit catalog) ``` diff --git a/src/adapters/getxapi-twitter.ts b/src/adapters/getxapi-twitter.ts new file mode 100644 index 0000000..88ed130 --- /dev/null +++ b/src/adapters/getxapi-twitter.ts @@ -0,0 +1,60 @@ +import type { Variant, PublishResult, ChannelHints } from "../models.js"; +import type { Profile } from "../backends/base.js"; + +const DEFAULT_GETXAPI_BASE_URL = "https://api.getxapi.com"; + +export class GetXAPITwitterAdapter { + hints(): ChannelHints { + return { + max_length: 280, + supported_md_features: ["links"], + cta_placement: "bottom", + canonical_url_supported: false, + browser_only: false, + }; + } + + async publish(variant: Variant, profile: Profile): Promise { + const apiKey = profile.credentials.GETXAPI_API_KEY; + if (!apiKey) { + return { channel: variant.channel, state: "failed", error: "GETXAPI_API_KEY required in profile" }; + } + + const enableActions = profile.credentials.GETXAPI_ENABLE_ACTIONS === "true"; + if (!enableActions) { + return { channel: variant.channel, state: "failed", error: "GETXAPI_ENABLE_ACTIONS must be true to publish writes" }; + } + + const baseUrl = (profile.credentials.GETXAPI_BASE_URL || DEFAULT_GETXAPI_BASE_URL).replace(/\/+$/, ""); + const text = variant.body.slice(0, 280); + + const res = await fetch(`${baseUrl}/twitter/tweet/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${apiKey}`, + }, + body: JSON.stringify({ text }), + }); + + if (!res.ok) { + const err = await res.text(); + return { channel: variant.channel, state: "failed", error: `GetXAPI publish failed: ${res.status} ${err.slice(0, 180)}` }; + } + + const data = await res.json() as { id?: string; tweet_id?: string; url?: string }; + const tweetId = data.id || data.tweet_id; + const liveUrl = data.url || (tweetId ? `https://x.com/i/web/status/${tweetId}` : ""); + + return { + channel: variant.channel, + state: "live", + live_url: liveUrl, + published_at: new Date().toISOString(), + }; + } + + async unpublish(_liveUrl: string, _profile: Profile): Promise<[boolean, string]> { + return [false, "GetXAPI tweet deletion not yet implemented"]; + } +} diff --git a/src/adapters/index.ts b/src/adapters/index.ts index e0bb1b5..85089f7 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -3,6 +3,7 @@ import { HashnodeAdapter } from "./hashnode.js"; import { GitHubDiscussionsAdapter } from "./github-discussions.js"; import { RedditAdapter } from "./reddit.js"; import { BlueskyAdapter } from "./bluesky.js"; +import { GetXAPITwitterAdapter } from "./getxapi-twitter.js"; import { makeBrowserAdapter } from "./browser.js"; import type { Variant, PublishResult, ChannelHints } from "../models.js"; import type { Profile } from "../backends/base.js"; @@ -35,5 +36,9 @@ export function buildAdapterMap(): Record { twitter_browser: twitter, "twitter-browser": twitter, x: twitter, + twitter_getxapi: new GetXAPITwitterAdapter(), + "twitter-getxapi": new GetXAPITwitterAdapter(), + getxapi_twitter: new GetXAPITwitterAdapter(), + "getxapi-twitter": new GetXAPITwitterAdapter(), }; }