Skill: parkertoddbrooks/wip-xai-x (version 1.0.4)

--- name: wip-xai-x version: "1.0.4" description: X Platform API. Read posts, search tweets, get bookmarks, post tweets, upload media. homepage: https://github.com/wipcomputer/wip-xai-x metadata: category: social,api api_base: https://api.x.com/2 capabilities: - api - fetch-posts - search-tweets - bookmarks - post-tweets - upload-media dependencies: - "@xdevplatform/xdk" interface: REST openclaw: emoji: "🐦" install: env: - X_BEARER_TOKEN - X_API_KEY - X_API_SECRET - X_ACCESS_TOKEN - X_ACCESS_TOKEN_SECRET author: name: Parker Todd Brooks --- # wip-xai-x X Platform API. Sensor (read) + Actuator (write). All X Platform functions in one tool. ## When to Use This Skill ### Sensor: Read **Use fetch_post for:** - Getting the full content of a specific tweet by URL or ID - Reading replies, quotes, engagement metrics - Extracting tweet data for processing **Use search_recent for:** - Finding tweets matching a query (last 7 days) - Searching by hashtags, mentions, or keywords - Getting raw tweet data (not AI-summarized... use wip-xai-grok search_x for that) **Use get_bookmarks for:** - Reading the user's bookmarked tweets - Reviewing saved content **Use get_user for:** - Looking up a user's profile, bio, follower count - Checking if an account exists ### Actuator: Write **Use post_tweet for:** - Posting new tweets - Replying to existing tweets - Quote-tweeting with commentary - Posting with images or video (upload first) **Use upload_media for:** - Uploading images (PNG, JPG, GIF, WebP) - Uploading video (MP4) - Getting media IDs for use in post_tweet **Use delete_tweet for:** - Removing a previously posted tweet **Use bookmark_post for:** - Saving a tweet for later ### Do NOT Use For - AI-powered search summarization (use wip-xai-grok search_web or search_x instead) - Image generation (use wip-xai-grok generate_image) - Video generation (use wip-xai-grok generate_video) - Trending topic analysis (use wip-xai-grok search_x for AI-summarized trends) ## API Reference ### fetch_post(options) ```javascript const result = await fetch_post({ id_or_url: 'https://x.com/user/status/123' }); // result: { data, includes, errors } ``` Options: id_or_url (required). Accepts tweet ID or full URL. ### search_recent(options) ```javascript const result = await search_recent({ query: 'AI agents', max_results: 20 }); // result: { data, includes, meta, errors } ``` Options: query (required), max_results (10-100), start_time, end_time, sort_order ### get_bookmarks(options) ```javascript const result = await get_bookmarks({ max_results: 20 }); ``` Options: max_results, pagination_token. Requires OAuth. ### get_user(options) ```javascript const result = await get_user({ username_or_id: 'parkertoddbrooks' }); ``` Options: username_or_id (required). Accepts username (with or without @) or numeric ID. ### post_tweet(options) ```javascript const result = await post_tweet({ text: 'Hello world', reply_to: '123', media_ids: ['456'] }); // result: { data: { id, text }, errors } ``` Options: text (required), reply_to, media_ids, quote_tweet_id. Requires OAuth. ### upload_media(options) ```javascript const result = await upload_media({ file_path: './photo.jpg' }); // result: { data: { id }, errors } ``` Options: file_path (required), media_type (auto-detected), media_data (base64 alternative) ### delete_tweet(options) ```javascript const result = await delete_tweet({ id: '123456' }); ``` Options: id (required). Requires OAuth. ## Troubleshooting ### "X Platform API credentials not found" Set X_BEARER_TOKEN for read-only, or all four OAuth tokens for read+write. 1Password: vault "Agent Secrets", item "X Platform API". ### "Could not resolve authenticated user ID" Bookmarks and write operations need OAuth 1.0a (all four tokens), not just bearer token. ### 403 Forbidden on post/delete Your app needs "Read and Write" permissions in the X Developer Portal. Check at https://developer.x.com/en/portal/dashboard ### Rate limits X API v2 has per-endpoint rate limits. The SDK handles rate limit headers automatically. If you hit limits, back off and retry. ## API Documentation - X API v2: https://docs.x.com/x-api - XDK SDK: https://github.com/xdevplatform/xdk - Authentication: https://docs.x.com/resources/authentication

path: CHANGELOG.md
content: # Changelog

## 1.0.4 (2026-03-16)

# wip-xai-x v1.0.4

Two fixes for MCP server startup:

1. v1.0.3: Added @modelcontextprotocol/sdk to dependencies (was imported but not declared). Closes wipcomputer/wip-xai-x#3
2. v1.0.4: Fixed schema imports (ListToolsRequestSchema/CallToolRequestSchema instead of method literals). Closes wipcomputer/wip-xai-x#4

## Issues closed

- Closes #4

## 1.0.3 (2026-03-16)

# wip-xai-x v1.0.3

Fix: add @modelcontextprotocol/sdk to dependencies. MCP server was failing with ERR_MODULE_NOT_FOUND when deployed via ldm install.

## Issues closed

- Closes #3

## 1.0.2 (2026-02-28)

- Rename wip-x to wip-xai-x across all internal references
- Add ai/ folder for private repo pattern

## 1.0.1 (2026-02-21)

Rename doors to interfaces in README. Auth config env vars.

## 1.0.0 (2026-02-20)

Initial release. X Platform API wrapper built on @xdevplatform/xdk.


path: README.md
content: ###### WIP Computer
# wip-xai-x

X Platform API. Read posts, search tweets, post, upload media.

Built on the official [@xdevplatform/xdk](https://github.com/xdevplatform/xdk) TypeScript SDK (v0.4.0, MIT, zero transitive deps).

Four interfaces: CLI, Module, MCP Server, Skill.

## Install

Open your AI coding tool and say:

```
Clone wipcomputer/wip-xai-x and read the README and SKILL.md.
Then explain to me:
1. What is this tool?
2. What does it do?
3. What would it change or fix in our current system?

Then ask me if I have more questions, or if I want you to integrate it into our system.
```

Your agent will read the repo, explain the tool, and walk you through integration interactively.

### Authentication

The tool resolves credentials automatically:

1. **1Password** (preferred) ... vault "Agent Secrets", item "X Platform API"
2. **Environment variables** ... as a fallback

**Read-only** (fetch posts, search, user lookup):
```bash
export X_BEARER_TOKEN="your-bearer-token"
```

**Read + Write** (post tweets, bookmarks, upload media):
```bash
export X_API_KEY="your-api-key"
export X_API_SECRET="your-api-secret"
export X_ACCESS_TOKEN="your-access-token"
export X_ACCESS_TOKEN_SECRET="your-access-token-secret"
```

Get credentials at https://developer.x.com/en/portal/dashboard

## Usage

### CLI

```bash
# Sensor (read)
wip-xai-x fetch https://x.com/parkertoddbrooks/status/123456
wip-xai-x search "AI agents" --max=20
wip-xai-x bookmarks --max=10
wip-xai-x user parkertoddbrooks
wip-xai-x me

# Actuator (write)
wip-xai-x post "Hello from wip-xai-x"
wip-xai-x post "Replying!" --reply-to=123456
wip-xai-x post "Check this out" --media=photo.jpg
wip-xai-x delete 123456
wip-xai-x bookmark 123456
wip-xai-x upload photo.jpg
```

### As a Module

```javascript
import { fetch_post, search_recent, post_tweet, get_bookmarks } from '@wipcomputer/wip-xai-x';

// Sensor: read
const post = await fetch_post({ id_or_url: 'https://x.com/user/status/123' });
console.log(post.data.text);

const results = await search_recent({ query: 'AI agents', max_results: 10 });
for (const tweet of results.data) {
  console.log(tweet.text);
}

const bookmarks = await get_bookmarks({ max_results: 20 });

// Actuator: write
const { data } = await post_tweet({ text: 'Hello from wip-xai-x' });
console.log(`Posted: https://x.com/i/status/${data.id}`);
```

### MCP Server

Add to your `.mcp.json`:

```json
{
  "wip-xai-x": {
    "command": "node",
    "args": ["/path/to/wip-xai-x/mcp-server.mjs"]
  }
}
```

Exposes 7 tools: x_fetch_post, x_search_recent, x_get_bookmarks, x_get_user, x_post_tweet, x_delete_tweet, x_upload_media.

## Functions

### Sensor (Read)

| Function | What | Auth |
|----------|------|------|
| `fetch_post` | Fetch a post by ID or URL | Bearer |
| `search_recent` | Search tweets (last 7 days) | Bearer |
| `get_bookmarks` | Get your bookmarks | OAuth |
| `get_user` | Get user profile | Bearer |
| `get_me` | Get your own profile | OAuth |

### Actuator (Write)

| Function | What | Auth |
|----------|------|------|
| `post_tweet` | Post a new tweet | OAuth |
| `delete_tweet` | Delete a tweet | OAuth |
| `bookmark_post` | Bookmark a post | OAuth |
| `unbookmark_post` | Remove a bookmark | OAuth |
| `upload_media` | Upload image/video/gif | OAuth |

**Bearer** = read-only bearer token. **OAuth** = OAuth 1.0a with all four tokens.

## wip-xai-x vs wip-xai-grok

| | wip-xai-x | wip-xai-grok |
|---|---|---|
| **API** | X Platform API (api.x.com) | xAI API (api.x.ai) |
| **Search** | Raw tweet data, exact matches | AI-powered search via Grok |
| **Read posts** | Fetch by ID, search by query | Not available |
| **Write** | Post tweets, upload media | Generate images/video |
| **Auth** | Bearer token / OAuth 1.0a | xAI API key |

Use **wip-xai-x** to read/write on X. Use **wip-xai-grok** for AI-powered search and media generation.

## Attribution

Built on [@xdevplatform/xdk](https://github.com/xdevplatform/xdk) v0.4.0 (official X TypeScript SDK).

---

## License

MIT

Built by Parker Todd Brooks, with Claude Code and Lēsa (OpenClaw).


path: _trash/RELEASE-NOTES-v1-0-3.md
content: # wip-xai-x v1.0.3

Fix: add @modelcontextprotocol/sdk to dependencies. MCP server was failing with ERR_MODULE_NOT_FOUND when deployed via ldm install.

## Issues closed

- Closes #3


path: _trash/RELEASE-NOTES-v1-0-4.md
content: # wip-xai-x v1.0.4

Two fixes for MCP server startup:

1. v1.0.3: Added @modelcontextprotocol/sdk to dependencies (was imported but not declared). Closes wipcomputer/wip-xai-x#3
2. v1.0.4: Fixed schema imports (ListToolsRequestSchema/CallToolRequestSchema instead of method literals). Closes wipcomputer/wip-xai-x#4

## Issues closed

- Closes #4


path: ai/todos/PUNCHLIST 2.md
content: # Punchlist

No current blockers.


path: ai/todos/PUNCHLIST.md
content: # Punchlist

No current blockers.


path: auth.mjs
content: /**
 * wip-x/auth.mjs
 * OAuth and token management for X Platform API.
 * Resolves credentials from 1Password or environment variables.
 */

import { execSync } from 'node:child_process';

const OP_VAULT = process.env.X_OP_VAULT || 'Agent Secrets';
const OP_ITEM = process.env.X_OP_ITEM || 'X API Key - wip-01';

/**
 * Read a field from 1Password.
 * Returns null if op CLI is not available or field not found.
 */
function opRead(field) {
  try {
    const ref = `op://${OP_VAULT}/${OP_ITEM}/${field}`;
    return execSync(`op read "${ref}" 2>/dev/null`, { encoding: 'utf8' }).trim() || null;
  } catch {
    return null;
  }
}

/**
 * Resolve all available credentials.
 * Priority: environment variables first, then 1Password.
 *
 * Environment variables:
 *   X_BEARER_TOKEN       ... app-only bearer token (read operations)
 *   X_API_KEY            ... OAuth 1.0a consumer key
 *   X_API_SECRET         ... OAuth 1.0a consumer secret
 *   X_ACCESS_TOKEN       ... OAuth 1.0a access token
 *   X_ACCESS_TOKEN_SECRET ... OAuth 1.0a access token secret
 *
 * 1Password (configurable via env):
 *   X_OP_VAULT  ... vault name (default: "Agent Secrets")
 *   X_OP_ITEM   ... item name (default: "X API Key - wip-01")
 *   Fields: bearer token, api key, api secret, access token, access token secret
 */
export function resolveAuth() {
  const auth = {
    bearerToken: process.env.X_BEARER_TOKEN || opRead('bearer token'),
    [REDACTED_SECRET] || opRead('api key'),
    apiSecret: process.env.X_API_SECRET || opRead('api secret'),
    accessToken: process.env.X_ACCESS_TOKEN || opRead('access token'),
    accessTokenSecret: process.env.X_ACCESS_TOKEN_SECRET || opRead('access token secret'),
  };

  if (!auth.bearerToken && !auth.apiKey) {
    throw new Error(
      'X Platform API credentials not found.\n' +
      'Set X_BEARER_TOKEN (for read-only) or X_API_KEY + X_API_SECRET + X_ACCESS_TOKEN + X_ACCESS_TOKEN_SECRET (for read/write).\n' +
      `Or add them to 1Password: vault "${OP_VAULT}", item "${OP_ITEM}".`
    );
  }

  return auth;
}

/**
 * Build the config object for the @xdevplatform/xdk Client constructor.
 * Uses OAuth 1.0a if all four tokens are available (needed for write operations).
 * Falls back to bearer token for read-only access.
 */
export async function getClientConfig(auth) {
  if (!auth) auth = resolveAuth();

  // If we have OAuth 1.0a credentials, use them (supports read + write)
  if (auth.apiKey && auth.apiSecret && auth.accessToken && auth.accessTokenSecret) {
    const { OAuth1 } = await import('@xdevplatform/xdk');
    const oauth1 = new OAuth1({
      [REDACTED_SECRET],
      apiSecret: auth.apiSecret,
      accessToken: auth.accessToken,
      accessTokenSecret: auth.accessTokenSecret,
    });
    return { oauth1 };
  }

  // Bearer token only (read operations)
  if (auth.bearerToken) {
    return { bearerToken: auth.bearerToken };
  }

  throw new Error('No valid auth configuration. Need bearer token or full OAuth 1.0a credentials.');
}


path: cli.mjs
content: #!/usr/bin/env node

/**
 * wip-x/cli.mjs
 * CLI for X Platform API. Sensor (read) + Actuator (write).
 */

import {
  fetch_post,
  search_recent,
  get_bookmarks,
  get_user,
  get_me,
  post_tweet,
  delete_tweet,
  bookmark_post,
  upload_media,
} from './core.mjs';

const args = process.argv.slice(2);
const command = args[0];

function flag(name) {
  const prefix = `--${name}=`;
  const found = args.find(a => a.startsWith(prefix));
  return found ? found.slice(prefix.length) : null;
}

function usage() {
  console.log(`wip-x ... X Platform API. Sensor + Actuator.

Sensor (read):
  wip-x fetch <url_or_id>                   Fetch a single post
  wip-x search <query> [--max=10]           Search recent tweets
  wip-x bookmarks [--max=20]                Get your bookmarks
  wip-x user <username>                      Get user profile
  wip-x me                                   Get your profile

Actuator (write):
  wip-x post <text> [--reply-to=id] [--media=file] [--quote=id]
  wip-x delete <id>                          Delete a tweet
  wip-x bookmark <tweet_id>                  Bookmark a post
  wip-x upload <file>                        Upload media, get media ID`);
}

async function main() {
  if (!command || command === '--help' || command === '-h') {
    usage();
    process.exit(0);
  }

  try {
    switch (command) {
      case 'fetch': {
        const target = args[1];
        if (!target) { console.error('Usage: wip-x fetch <url_or_id>'); process.exit(1); }
        const result = await fetch_post({ id_or_url: target });
        console.log(JSON.stringify(result, null, 2));
        break;
      }

      case 'search': {
        const query = args[1];
        if (!query) { console.error('Usage: wip-x search <query>'); process.exit(1); }
        const max = flag('max') ? parseInt(flag('max'), 10) : undefined;
        const result = await search_recent({ query, max_results: max });
        if (result.data) {
          for (const tweet of result.data) {
            const author = result.includes?.users?.find(u => u.id === tweet.author_id);
            console.log(`@${author?.username || tweet.author_id}: ${tweet.text}`);
            console.log(`  ${tweet.created_at}  id:${tweet.id}`);
            console.log();
          }
          console.log(`${result.data.length} results`);
        } else {
          console.log('No results.');
        }
        break;
      }

      case 'bookmarks': {
        const max = flag('max') ? parseInt(flag('max'), 10) : undefined;
        const result = await get_bookmarks({ max_results: max });
        if (result.data) {
          for (const tweet of result.data) {
            const author = result.includes?.users?.find(u => u.id === tweet.author_id);
            console.log(`@${author?.username || tweet.author_id}: ${tweet.text}`);
            console.log(`  ${tweet.created_at}  id:${tweet.id}`);
            console.log();
          }
          console.log(`${result.data.length} bookmarks`);
        } else {
          console.log('No bookmarks.');
        }
        break;
      }

      case 'user': {
        const target = args[1];
        if (!target) { console.error('Usage: wip-x user <username>'); process.exit(1); }
        const result = await get_user({ username_or_id: target });
        if (result.data) {
          const u = result.data;
          console.log(`@${u.username} (${u.name})`);
          if (u.description) console.log(u.description);
          if (u.public_metrics) {
            console.log(`Followers: ${u.public_metrics.followers_count}  Following: ${u.public_metrics.following_count}  Posts: ${u.public_metrics.tweet_count}`);
          }
          console.log(`ID: ${u.id}  Created: ${u.created_at}`);
        } else {
          console.log('User not found.');
        }
        break;
      }

      case 'me': {
        const result = await get_me();
        if (result.data) {
          const u = result.data;
          console.log(`@${u.username} (${u.name})`);
          if (u.description) console.log(u.description);
          if (u.public_metrics) {
            console.log(`Followers: ${u.public_metrics.followers_count}  Following: ${u.public_metrics.following_count}  Posts: ${u.public_metrics.tweet_count}`);
          }
          console.log(`ID: ${u.id}`);
        }
        break;
      }

      case 'post': {
        const text = args[1];
        if (!text) { console.error('Usage: wip-x post <text>'); process.exit(1); }
        const replyTo = flag('reply-to');
        const quote = flag('quote');
        const mediaFile = flag('media');

        let mediaIds;
        if (mediaFile) {
          const upload = await upload_media({ file_path: mediaFile });
          if (upload.data?.id) mediaIds = [upload.data.id];
        }

        const result = await post_tweet({ text, reply_to: replyTo, media_ids: mediaIds, quote_tweet_id: quote });
        if (result.data) {
          console.log(`Posted: https://x.com/i/status/${result.data.id}`);
        } else {
          console.error('Failed to post:', result.errors);
        }
        break;
      }

      case 'delete': {
        const id = args[1];
        if (!id) { console.error('Usage: wip-x delete <id>'); process.exit(1); }
        const result = await delete_tweet({ id });
        console.log(result.data?.deleted ? 'Deleted.' : 'Failed to delete.');
        break;
      }

      case 'bookmark': {
        const tweetId = args[1];
        if (!tweetId) { console.error('Usage: wip-x bookmark <tweet_id>'); process.exit(1); }
        const result = await bookmark_post({ tweet_id: tweetId });
        console.log(result.data?.bookmarked ? 'Bookmarked.' : 'Failed to bookmark.');
        break;
      }

      case 'upload': {
        const file = args[1];
        if (!file) { console.error('Usage: wip-x upload <file>'); process.exit(1); }
        const result = await upload_media({ file_path: file });
        if (result.data) {
          console.log(`Media ID: ${result.data.id}`);
        } else {
          console.error('Upload failed:', result.errors);
        }
        break;
      }

      default:
        console.error(`Unknown command: ${command}`);
        usage();
        process.exit(1);
    }
  } catch (err) {
    console.error(`Error: ${err.message}`);
    process.exit(1);
  }
}

main();


path: core.mjs
content: /**
 * wip-x/core.mjs
 * X Platform API wrapper. Sensor (read) + Actuator (write).
 * Built on @xdevplatform/xdk (official X TypeScript SDK).
 */

import { Client } from '@xdevplatform/xdk';
import { resolveAuth, getClientConfig } from './auth.mjs';
import { readFileSync } from 'node:fs';

// Default fields to request for rich post data
const DEFAULT_TWEET_FIELDS = ['created_at', 'author_id', 'conversation_id', 'public_metrics', 'entities'];
const DEFAULT_USER_FIELDS = ['created_at', 'description', 'public_metrics', 'profile_image_url', 'verified'];
const DEFAULT_EXPANSIONS = ['author_id'];

let _client = null;
let _userId = null;

/**
 * Get or create a configured Client instance.
 */
async function getClient() {
  if (_client) return _client;
  const config = await getClientConfig();
  _client = new Client(config);
  return _client;
}

/**
 * Get the authenticated user's ID (needed for bookmarks, etc).
 */
async function getMyUserId() {
  if (_userId) return _userId;
  const client = await getClient();
  const me = await client.users.getMe({ userFields: DEFAULT_USER_FIELDS });
  _userId = me.data?.id;
  if (!_userId) throw new Error('Could not resolve authenticated user ID. Check your credentials.');
  return _userId;
}

// ---------------------------------------------------------------------------
// Sensor (Read)
// ---------------------------------------------------------------------------

/**
 * Fetch a single post by ID or URL.
 * Accepts a tweet ID or a full URL like https://x.com/user/status/123456
 */
export async function fetch_post({ id_or_url, tweetFields, userFields, expansions } = {}) {
  if (!id_or_url) throw new Error('id_or_url is required');

  // Extract ID from URL if needed
  const id = extractTweetId(id_or_url);
  const client = await getClient();

  const result = await client.posts.getById(id, {
    tweetFields: tweetFields || DEFAULT_TWEET_FIELDS,
    userFields: userFields || DEFAULT_USER_FIELDS,
    expansions: expansions || DEFAULT_EXPANSIONS,
  });

  return {
    data: result.data,
    includes: result.includes,
    errors: result.errors,
  };
}

/**
 * Search recent tweets (last 7 days).
 */
export async function search_recent({ query, max_results, start_time, end_time, sort_order } = {}) {
  if (!query) throw new Error('query is required');
  const client = await getClient();

  const opts = {
    tweetFields: DEFAULT_TWEET_FIELDS,
    userFields: DEFAULT_USER_FIELDS,
    expansions: DEFAULT_EXPANSIONS,
  };
  if (max_results) opts.maxResults = max_results;
  if (start_time) opts.startTime = start_time;
  if (end_time) opts.endTime = end_time;
  if (sort_order) opts.sortOrder = sort_order;

  const result = await client.posts.searchRecent(query, opts);

  return {
    data: result.data,
    includes: result.includes,
    meta: result.meta,
    errors: result.errors,
  };
}

/**
 * Get bookmarks for the authenticated user.
 */
export async function get_bookmarks({ max_results, pagination_token } = {}) {
  const client = await getClient();
  const userId = await getMyUserId();

  const opts = {
    tweetFields: DEFAULT_TWEET_FIELDS,
    userFields: DEFAULT_USER_FIELDS,
    expansions: DEFAULT_EXPANSIONS,
  };
  if (max_results) opts.maxResults = max_results;
  if (pagination_token) opts.paginationToken = pagination_token;

  const result = await client.users.getBookmarks(userId, opts);

  return {
    data: result.data,
    includes: result.includes,
    meta: result.meta,
    errors: result.errors,
  };
}

/**
 * Get user info by username or ID.
 */
export async function get_user({ username_or_id } = {}) {
  if (!username_or_id) throw new Error('username_or_id is required');
  const client = await getClient();

  // If it starts with a digit, treat as ID
  if (/^\d+$/.test(username_or_id)) {
    const result = await client.users.getById(username_or_id, {
      userFields: DEFAULT_USER_FIELDS,
    });
    return { data: result.data, errors: result.errors };
  }

  // Strip @ if present
  const username = username_or_id.replace(/^@/, '');
  const result = await client.users.getByUsername(username, {
    userFields: DEFAULT_USER_FIELDS,
  });
  return { data: result.data, errors: result.errors };
}

/**
 * Get the authenticated user's profile.
 */
export async function get_me() {
  const client = await getClient();
  const result = await client.users.getMe({
    userFields: DEFAULT_USER_FIELDS,
  });
  return { data: result.data, errors: result.errors };
}

// ---------------------------------------------------------------------------
// Actuator (Write)
// ---------------------------------------------------------------------------

/**
 * Post a tweet.
 */
export async function post_tweet({ text, reply_to, media_ids, quote_tweet_id } = {}) {
  if (!text) throw new Error('text is required');
  const client = await getClient();

  const body = { text };
  if (reply_to) body.reply = { inReplyToTweetId: reply_to };
  if (media_ids && media_ids.length > 0) body.media = { mediaIds: media_ids };
  if (quote_tweet_id) body.quoteTweetId = quote_tweet_id;

  const result = await client.posts.create(body);
  return { data: result.data, errors: result.errors };
}

/**
 * Delete a tweet.
 */
export async function delete_tweet({ id } = {}) {
  if (!id) throw new Error('id is required');
  const client = await getClient();
  const result = await client.posts.delete(id);
  return { data: result.data, errors: result.errors };
}

/**
 * Bookmark a post.
 */
export async function bookmark_post({ tweet_id } = {}) {
  if (!tweet_id) throw new Error('tweet_id is required');
  const client = await getClient();
  const userId = await getMyUserId();
  const result = await client.users.createBookmark(userId, { tweetId: tweet_id });
  return { data: result.data, errors: result.errors };
}

/**
 * Remove a bookmark.
 */
export async function unbookmark_post({ tweet_id } = {}) {
  if (!tweet_id) throw new Error('tweet_id is required');
  const client = await getClient();
  const userId = await getMyUserId();
  const result = await client.users.deleteBookmark(userId, tweet_id);
  return { data: result.data, errors: result.errors };
}

/**
 * Upload media (image, video, gif).
 * The X API v2 media upload takes base64-encoded data.
 */
export async function upload_media({ file_path, media_data, media_type, alt_text } = {}) {
  if (!file_path && !media_data) throw new Error('file_path or media_data is required');
  const client = await getClient();

  let data = media_data;
  if (file_path && !data) {
    const buffer = readFileSync(file_path);
    data = buffer.toString('base64');
  }

  // Detect media type from extension if not provided
  if (!media_type && file_path) {
    const ext = file_path.split('.').pop().toLowerCase();
    const types = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', mp4: 'video/mp4', webp: 'image/webp' };
    media_type = types[ext] || 'application/octet-stream';
  }

  const body = {
    mediaData: data,
    mediaType: media_type,
  };

  const result = await client.media.upload({ body });

  // Set alt text if provided and we got a media ID
  if (alt_text && result.data?.id) {
    try {
      // Alt text would need a separate metadata endpoint... SDK may not expose it yet
      // For now we return without alt text
    } catch { /* ignore */ }
  }

  return { data: result.data, errors: result.errors };
}

// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------

/**
 * Extract tweet ID from a URL or return as-is if already an ID.
 * Handles: https://x.com/user/status/123, https://twitter.com/user/status/123, plain 123
 */
function extractTweetId(input) {
  if (/^\d+$/.test(input)) return input;
  const match = input.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/);
  if (match) return match[1];
  throw new Error(`Cannot extract tweet ID from: ${input}`);
}

/**
 * Reset the cached client (useful for testing or credential rotation).
 */
export function resetClient() {
  _client = null;
  _userId = null;
}


path: mcp-server.mjs
content: #!/usr/bin/env node

/**
 * wip-x/mcp-server.mjs
 * MCP server exposing X Platform API as tools.
 * 7 tools: x_fetch_post, x_search_recent, x_get_bookmarks, x_get_user,
 *          x_post_tweet, x_delete_tweet, x_upload_media
 */

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import {
  fetch_post,
  search_recent,
  get_bookmarks,
  get_user,
  post_tweet,
  delete_tweet,
  upload_media,
} from './core.mjs';

const server = new Server(
  { name: 'wip-x', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

const TOOLS = [
  {
    name: 'x_fetch_post',
    description: 'Fetch a single X/Twitter post by ID or URL. Returns full post data with author info.',
    inputSchema: {
      type: 'object',
      properties: {
        id_or_url: { type: 'string', description: 'Tweet ID or full URL (e.g. https://x.com/user/status/123)' },
      },
      required: ['id_or_url'],
    },
  },
  {
    name: 'x_search_recent',
    description: 'Search recent tweets (last 7 days). Uses X API v2 search endpoint.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query (supports X search operators)' },
        max_results: { type: 'number', description: 'Max results (10-100, default 10)' },
        start_time: { type: 'string', description: 'Start time (ISO 8601)' },
        end_time: { type: 'string', description: 'End time (ISO 8601)' },
      },
      required: ['query'],
    },
  },
  {
    name: 'x_get_bookmarks',
    description: 'Get bookmarked posts for the authenticated user.',
    inputSchema: {
      type: 'object',
      properties: {
        max_results: { type: 'number', description: 'Max results (default 20)' },
      },
    },
  },
  {
    name: 'x_get_user',
    description: 'Get user profile by username or ID.',
    inputSchema: {
      type: 'object',
      properties: {
        username_or_id: { type: 'string', description: 'Username (with or without @) or numeric user ID' },
      },
      required: ['username_or_id'],
    },
  },
  {
    name: 'x_post_tweet',
    description: 'Post a new tweet. Requires OAuth credentials (not just bearer token).',
    inputSchema: {
      type: 'object',
      properties: {
        text: { type: 'string', description: 'Tweet text (max 280 characters)' },
        reply_to: { type: 'string', description: 'Tweet ID to reply to' },
        quote_tweet_id: { type: 'string', description: 'Tweet ID to quote' },
        media_ids: { type: 'array', items: { type: 'string' }, description: 'Media IDs to attach (upload first with x_upload_media)' },
      },
      required: ['text'],
    },
  },
  {
    name: 'x_delete_tweet',
    description: 'Delete a tweet by ID. Requires OAuth credentials.',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Tweet ID to delete' },
      },
      required: ['id'],
    },
  },
  {
    name: 'x_upload_media',
    description: 'Upload media (image/video/gif) for use in tweets. Returns a media ID.',
    inputSchema: {
      type: 'object',
      properties: {
        file_path: { type: 'string', description: 'Path to the file to upload' },
        media_type: { type: 'string', description: 'MIME type (auto-detected from extension if omitted)' },
      },
      required: ['file_path'],
    },
  },
];

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: TOOLS,
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: params } = request.params;

  try {
    let result;
    switch (name) {
      case 'x_fetch_post':
        result = await fetch_post(params);
        break;
      case 'x_search_recent':
        result = await search_recent(params);
        break;
      case 'x_get_bookmarks':
        result = await get_bookmarks(params);
        break;
      case 'x_get_user':
        result = await get_user(params);
        break;
      case 'x_post_tweet':
        result = await post_tweet(params);
        break;
      case 'x_delete_tweet':
        result = await delete_tweet(params);
        break;
      case 'x_upload_media':
        result = await upload_media(params);
        break;
      default:
        return {
          content: [{ type: 'text', text: `Unknown tool: ${name}` }],
          isError: true,
        };
    }

    return {
      content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
    };
  } catch (err) {
    return {
      content: [{ type: 'text', text: `Error: ${err.message}` }],
      isError: true,
    };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);


path: package-lock.json
content: {
  "name": "@wipcomputer/wip-xai-x",
  "version": "1.0.2",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "@wipcomputer/wip-xai-x",
      "version": "1.0.2",
      "license": "MIT",
      "dependencies": {
        "@modelcontextprotocol/sdk": "^1.27.1",
        "@xdevplatform/xdk": "^0.4.0"
      },
      "bin": {
        "wip-xai-x": "cli.mjs"
      }
    },
    "node_modules/@hono/node-server": {
      "version": "1.19.11",
      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.14.1"
      },
      "peerDependencies": {
        "hono": "^4"
      }
    },
    "node_modules/@modelcontextprotocol/sdk": {
      "version": "1.27.1",
      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@hono/node-server": "^1.19.9",
        "ajv": "^8.17.1",
        "ajv-formats": "^3.0.1",
        "content-type": "^1.0.5",
        "cors": "^2.8.5",
        "cross-spawn": "^7.0.5",
        "eventsource": "^3.0.2",
        "eventsource-parser": "^3.0.0",
        "express": "^5.2.1",
        "express-rate-limit": "^8.2.1",
        "hono": "^4.11.4",
        "jose": "^6.1.3",
        "json-schema-typed": "^8.0.2",
        "pkce-challenge": "^5.0.0",
        "raw-body": "^3.0.0",
        "zod": "^3.25 || ^4.0",
        "zod-to-json-schema": "^3.25.1"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@cfworker/json-schema": "^4.1.1",
        "zod": "^3.25 || ^4.0"
      },
      "peerDependenciesMeta": {
        "@cfworker/json-schema": {
          "optional": true
        },
        "zod": {
          "optional": false
        }
      }
    },
    "node_modules/@xdevplatform/xdk": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/@xdevplatform/xdk/-/xdk-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.14"
      },
      "peerDependencies": {
        "node-fetch": "^3.3.0"
      },
      "peerDependenciesMeta": {
        "node-fetch": {
          "optional": true
        }
      }
    },
    "node_modules/accepts": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "^3.0.0",
        "negotiator": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/ajv": {
      "version": "8.18.0",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fast-deep-equal": "^3.1.3",
        "fast-uri": "^3.0.1",
        "json-schema-traverse": "^1.0.0",
        "require-from-string": "^2.0.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/ajv-formats": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependenciesMeta": {
        "ajv": {
          "optional": true
        }
      }
    },
    "node_modules/body-parser": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "^3.1.2",
        "content-type": "^1.0.5",
        "debug": "^4.4.3",
        "http-errors": "^2.0.0",
        "iconv-lite": "^0.7.0",
        "on-finished": "^2.4.1",
        "qs": "^6.14.1",
        "raw-body": "^3.0.1",
        "type-is": "^2.0.1"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/content-disposition": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6.6.0"
      }
    },
    "node_modules/cors": {
      "version": "2.8.6",
      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "object-assign": "^4",
        "vary": "^1"
      },
      "engines": {
        "node": ">= 0.10"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/eventsource": {
      "version": "3.0.7",
      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "eventsource-parser": "^3.0.1"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/eventsource-parser": {
      "version": "3.0.6",
      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/express": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "accepts": "^2.0.0",
        "body-parser": "^2.2.1",
        "content-disposition": "^1.0.0",
        "content-type": "^1.0.5",
        "cookie": "^0.7.1",
        "cookie-signature": "^1.2.1",
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "finalhandler": "^2.1.0",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.0",
        "merge-descriptors": "^2.0.0",
        "mime-types": "^3.0.0",
        "on-finished": "^2.4.1",
        "once": "^1.4.0",
        "parseurl": "^1.3.3",
        "proxy-addr": "^2.0.7",
        "qs": "^6.14.0",
        "range-parser": "^1.2.1",
        "router": "^2.2.0",
        "send": "^1.1.0",
        "serve-static": "^2.2.0",
        "statuses": "^2.0.1",
        "type-is": "^2.0.1",
        "vary": "^1.1.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/express-rate-limit": {
      "version": "8.3.1",
      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ip-address": "10.1.0"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/express-rate-limit"
      },
      "peerDependencies": {
        "express": ">= 4.11"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fast-uri": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fastify"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/fastify"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/finalhandler": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "on-finished": "^2.4.1",
        "parseurl": "^1.3.3",
        "statuses": "^2.0.1"
      },
      "engines": {
        "node": ">= 18.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/hono": {
      "version": "4.12.8",
      "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.9.0"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ip-address": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-promise": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/jose": {
      "version": "6.2.1",
      "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/panva"
      }
    },
    "node_modules/json-schema-traverse": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/json-schema-typed": {
      "version": "8.0.2",
      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/mime-db": {
      "version": "1.54.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "^1.54.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/negotiator": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/object-assign": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "8.3.0",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/pkce-challenge": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.20.0"
      }
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/qs": {
      "version": "6.15.0",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.7.0",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/require-from-string": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/router": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "is-promise": "^4.0.0",
        "parseurl": "^1.3.3",
        "path-to-regexp": "^8.0.0"
      },
      "engines": {
        "node": ">= 18"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.3",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.1",
        "mime-types": "^3.0.2",
        "ms": "^2.1.3",
        "on-finished": "^2.4.1",
        "range-parser": "^1.2.1",
        "statuses": "^2.0.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/serve-static": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "parseurl": "^1.3.3",
        "send": "^1.2.0"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/type-is": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "content-type": "^1.0.5",
        "media-typer": "^1.1.0",
        "mime-types": "^3.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/zod": {
      "version": "4.3.6",
      "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/colinhacks"
      }
    },
    "node_modules/zod-to-json-schema": {
      "version": "3.25.1",
      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "peerDependencies": {
        "zod": "^3.25 || ^4"
      }
    }
  }
}


path: package.json
content: {
  "name": "@wipcomputer/wip-xai-x",
  "version": "1.0.4",
  "description": "X Platform API. Sensor (read posts, search, bookmarks) + Actuator (post tweets, upload media).",
  "type": "module",
  "main": "core.mjs",
  "bin": {
    "wip-xai-x": "./cli.mjs"
  },
  "exports": {
    ".": "./core.mjs",
    "./cli": "./cli.mjs",
    "./auth": "./auth.mjs"
  },
  "scripts": {
    "test": "node -e \"import('./core.mjs').then(m => console.log('Exports:', Object.keys(m).join(', ')))\""
  },
  "keywords": [
    "x",
    "twitter",
    "api",
    "sensor",
    "actuator",
    "mcp",
    "agent"
  ],
  "author": "Parker Todd Brooks",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/wipcomputer/wip-xai-x.git"
  },
  "homepage": "https://github.com/wipcomputer/wip-xai-x",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.27.1",
    "@xdevplatform/xdk": "^0.4.0"
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (HIGH)