# hitpost-pro

Adopt this skill: https://proskills.md/skills/other/lihaila-hitpost-skill-skills-hitpost-pro

Page: https://proskills.md/skills/other/lihaila-hitpost-skill-skills-hitpost-pro
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: skills/hitpost-pro/SKILL.md
size: 5278
sha: 738e473dcc77e35ccf89639c11b6634537148395
upstream: https://github.com/lihaila/hitpost-skill/blob/main/skills/hitpost-pro/SKILL.md

````text
---
name: hitpost-pro
description: Run HitPost (lihaila.si) from your AI agent. It writes X (Twitter) posts, threads, long posts and X Articles from live research (Grok reads what X is saying this week, Brave checks the web and news), polishes drafts with a blind multi-model judge, and posts them to the user's own X account. Uses the user's HitPost account and credits through an API key. Use it whenever the user has a HitPost account or key, mentions HitPost Pro, or wants live X research, an X Article, or a post published to X from the agent, even if they don't say "HitPost".
---

# HitPost Pro

This skill runs the real HitPost pipeline on the user's HitPost account. You don't write the posts yourself here: HitPost researches live and writes them, and you help the user choose, polish and post. You're working for the user, so the posts go to **their own followers**.

Everything goes through one script, `scripts/hitpost.mjs` (Node 18+, no dependencies). It prints JSON and sends progress to stderr. Run it as `node <skill-dir>/scripts/hitpost.mjs <command>`.

## Accounts, credits and keys

- HitPost is for **blue-check X accounts**. The user signs in with X at lihaila.si, then creates a key under **account menu → Use in AI agents**.
- New accounts get free credits once. After that, they **top up with Stripe on the website** (account menu → Top up). Credits are shared between the site and this skill.
- **Costs:** a craft (live research plus three drafts, or one X Article) uses 1 credit. Optimize and format are free within a daily allowance. Posting is free, except that a post containing a link uses 1 credit.
- A craft that fails on HitPost's side is refunded automatically.

## First use

Run `me`. If it exits with code 2, the JSON's `action` says what the user has to do. Tell them in plain words and stop until they've done it:

| action | Tell the user |
|---|---|
| `create_key` | Sign in with X at lihaila.si, open the account menu → **Use in AI agents** → **Create a key**, and paste the key here. Then run `login <key>`. The key is saved to `~/.config/hitpost/key`, readable only by them. |
| `blue_check` | HitPost needs a blue-check X account (X Premium; Basic doesn't include a check mark). |
| `top_up` | They're out of credits: top up at lihaila.si → account menu → **Top up**, then try again. |
| `reconnect_x` | X access expired: sign in with X again at lihaila.si. |

Never ask for, or handle, their X password or card details. Sign-in and payment happen only on the website.

## Writing posts: craft

```bash
node hitpost.mjs craft --idea "<the user's idea, in their words>" [--format single|thread|long|article] [--tone auto|insight|story|bold|funny|pro] [--lang auto|en|zh|…]
```

- **Format:** `single` is the default. Use `thread` for "thread" or 🧵, `long` for a long Premium post, and `article` for an X Article. Tone and language default to `auto`.
- **Pass the idea as the user wrote it.** HitPost decides on its own whether the idea needs research.
- **Tell the user before running it** that a craft takes about 1–2 minutes and uses 1 credit. Show the balance from `me` if you have it.
- **The result:** `craftId`, `research` (what X is saying, angles, top posts and sources), and `variants`. Each variant has `angle`, `why` and `posts`. An article variant has `titles` and a Markdown `body` instead.
- **Show the user** each variant's angle and full text, plus a short "What HitPost found" from `research` with the source links, and the remaining `credits`.
- **Keep the draft text exactly as HitPost returned it**, unless the user asks you to change it.

## Polishing: optimize (free, daily allowance)

Write the posts to a JSON file (an array of strings, one per post), then run:

```bash
node hitpost.mjs optimize --posts posts.json --craft <craftId>
```

DeepSeek and Qwen each rewrite the draft, and Grok scores all versions blind on hook, clarity, value, shareability and how human they sound. Show the winner with its scores and notes, and the other versions in case the user prefers one. Pass `--craft` whenever the draft came from a craft, so the rewrites use its research.

`format --posts posts.json --mode emojis|bullets` adds a few emojis or turns the posts into bullets. The wording stays the same.

## Posting: post

```bash
node hitpost.mjs post --posts posts.json [--craft <craftId>] [--variant <id>]
```

Posting publishes to the user's X account. It's public and hard to take back, so:

1. **Show the exact text** that will be posted, every post of the thread, and **ask for a clear yes**. Approval to post one draft doesn't carry over to the next.
2. If any post contains a link, say that it uses 1 credit per post with a link. Offer to remove the link and keep the source name instead.
3. After posting, give the user the link to each post from `posted`.

The limit is 50 posts a day, counting every post of a thread. Images, replies and likes aren't available through the skill; those stay on the website.

## When something fails

- Exit code 2 means the user has to act: see the table above.
- Exit code 1 is an error. Show the `error` message. A failed craft refunds its credit, so suggest trying again in a minute.
- Don't retry a craft automatically, because each run uses a credit.
````

## File: skills/hitpost-pro/scripts/hitpost.mjs
size: 8509
sha: 5d7f8b2e84860e14ac1620955c65cc15a8642c85
upstream: https://github.com/lihaila/hitpost-skill/blob/main/skills/hitpost-pro/scripts/hitpost.mjs

```text
#!/usr/bin/env node
// HitPost API client for the HitPost Pro skill. No dependencies; Node 18+.
//
//   node hitpost.mjs login <key>          save the API key (from lihaila.si → account menu → Use in AI agents)
//   node hitpost.mjs me                   account, credits and today's optimizations left
//   node hitpost.mjs craft --idea "..." [--format single|thread|long|article] [--tone auto|insight|story|bold|funny|pro] [--lang auto|en|zh|ja|ko|es|fr|de|pt] [--handle you]
//   node hitpost.mjs optimize --posts posts.json [--craft <craftId>]
//   node hitpost.mjs format --posts posts.json --mode emojis|bullets
//   node hitpost.mjs post --posts posts.json [--craft <craftId>] [--variant <id>]
//
// posts.json is a JSON array of post strings (one per post of a thread).
// Output is JSON on stdout; progress goes to stderr. Exit codes: 0 ok, 1 error, 2 needs action
// (no key, out of credits, not a blue-check account): the JSON's "action" says what to do.
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

const BASE = (process.env.HITPOST_URL || 'https://lihaila.si').replace(/\/+$/, '');
const KEY_FILE = path.join(os.homedir(), '.config', 'hitpost', 'key');
const TOPUP = `${BASE}/ (account menu → Top up)`;
const KEYS = `${BASE}/ (sign in with X → account menu → Use in AI agents → Create a key)`;

const out = (obj, code = 0) => { process.stdout.write(JSON.stringify(obj, null, 2) + '\n'); process.exit(code); };
const needs = (action, message) => out({ ok: false, action, message }, 2);

function args(list) {
  const o = { _: [] };
  for (let i = 0; i < list.length; i++) {
    if (list[i].startsWith('--')) o[list[i].slice(2)] = list[i + 1]?.startsWith('--') || list[i + 1] === undefined ? true : list[++i];
    else o._.push(list[i]);
  }
  return o;
}

function readKey() {
  if (process.env.HITPOST_API_KEY) return process.env.HITPOST_API_KEY.trim();
  try { return fs.readFileSync(KEY_FILE, 'utf8').trim(); } catch { return ''; }
}

function readPosts(file) {
  if (!file || file === true) out({ ok: false, error: 'Pass --posts <file.json> (a JSON array of post strings).' }, 1);
  let posts;
  try { posts = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { out({ ok: false, error: `Couldn't read ${file}: ${e.message}` }, 1); }
  if (!Array.isArray(posts) || !posts.length || posts.some((p) => typeof p !== 'string')) out({ ok: false, error: `${file} must be a JSON array of post strings.` }, 1);
  return posts;
}

/** Turn a refused request into what the user has to do. */
function refused(status, body) {
  const msg = body?.error || `HTTP ${status}`;
  if (body?.code === 'badkey' || status === 401) needs('create_key', `${msg} Create one at ${KEYS}, then run: node hitpost.mjs login <key>`);
  if (body?.code === 'verified' || body?.code === 'signin') needs('blue_check', msg);
  if (body?.code === 'quota' || body?.code === 'linkcredits' || status === 402) needs('top_up', `${msg} Top up at ${TOPUP}; credits are shared with the website.`);
  out({ ok: false, status, error: msg }, 1);
}

async function call(p, { method = 'GET', body } = {}) {
  const key = readKey();
  if (!key) needs('create_key', `No HitPost API key yet. Create one at ${KEYS}, then run: node hitpost.mjs login <key>`);
  let res;
  try {
    res = await fetch(BASE + p, {
      method,
      headers: { authorization: `Bearer ${key}`, ...(body ? { 'content-type': 'application/json' } : {}) },
      body: body ? JSON.stringify(body) : undefined,
      signal: AbortSignal.timeout(400_000),
    });
  } catch (e) {
    out({ ok: false, error: `Couldn't reach ${BASE}: ${e.message}` }, 1);
  }
  return res;
}

/** Long jobs stream newline-delimited JSON events; returns them all, logging steps to stderr. */
async function streamed(res) {
  const events = [];
  const dec = new TextDecoder();
  let buf = '';
  for await (const chunk of res.body) {
    buf += dec.decode(chunk, { stream: true });
    let i;
    while ((i = buf.indexOf('\n')) >= 0) {
      const line = buf.slice(0, i).trim();
      buf = buf.slice(i + 1);
      if (!line) continue;
      const ev = JSON.parse(line);
      if (ev.type === 'step') process.stderr.write(`[hitpost] ${ev.step}…\n`);
      events.push(ev);
    }
  }
  return events;
}

const credits = (q) => (q ? { credits: q.credits, optimizesLeftToday: q.optimizes } : undefined);

const [cmd, ...rest] = process.argv.slice(2);
const a = args(rest);

switch (cmd) {
  case 'login': {
    const key = String(a._[0] || '').trim();
    if (!/^hp_[A-Za-z0-9_-]{20,}$/.test(key)) out({ ok: false, error: 'That doesn\'t look like a HitPost key (it starts with hp_).' }, 1);
    fs.mkdirSync(path.dirname(KEY_FILE), { recursive: true });
    fs.writeFileSync(KEY_FILE, key + '\n', { mode: 0o600 });
    const res = await call('/api/me');
    const body = await res.json().catch(() => ({}));
    if (!res.ok) refused(res.status, body);
    out({ ok: true, saved: KEY_FILE, handle: body.user?.handle, ...credits(body.quota) });
  }
  case 'me': {
    const res = await call('/api/me');
    const body = await res.json().catch(() => ({}));
    if (!res.ok) refused(res.status, body);
    out({ ok: true, handle: body.user?.handle, blueCheck: body.user?.verified, xConnected: body.user?.connected, ...credits(body.quota), topUp: TOPUP });
  }
  case 'craft': {
    if (!a.idea || a.idea === true) out({ ok: false, error: 'Pass --idea "what the post is about".' }, 1);
    const res = await call('/api/craft', { method: 'POST', body: { idea: a.idea, format: a.format, tone: a.tone, lang: a.lang, handle: a.handle } });
    if (!res.ok) refused(res.status, await res.json().catch(() => ({})));
    const events = await streamed(res);
    const start = events.find((e) => e.type === 'start');
    const research = events.find((e) => e.type === 'research')?.research;
    const done = events.find((e) => e.type === 'variants');
    const err = events.find((e) => e.type === 'error');
    if (!done) out({ ok: false, error: err?.error || 'The craft did not finish. Your credit was refunded.', ...credits(err?.quota) }, 1);
    out({
      ok: true,
      craftId: start?.id,
      // What the research found, for citing and for the user's judgment.
      research: research && !research.light ? {
        pulse: research.x?.pulse || '',
        angles: research.x?.angles || [],
        hotPosts: (research.x?.hot_posts || []).map((p) => ({ author: p.author, gist: p.gist, url: p.url })),
        sources: (research.web || []).map((w) => ({ source: w.source, title: w.title, url: w.url })),
      } : { kind: research?.kind || 'light' },
      variants: done.variants,
      ...credits(done.quota),
    });
  }
  case 'optimize': {
    const res = await call('/api/optimize', { method: 'POST', body: { posts: readPosts(a.posts), craftId: a.craft } });
    if (!res.ok) refused(res.status, await res.json().catch(() => ({})));
    const events = await streamed(res);
    const r = events.find((e) => e.type === 'result');
    if (!r) out({ ok: false, error: events.find((e) => e.type === 'error')?.error || 'Optimization failed.' }, 1);
    out({ ok: true, winner: r.winner, candidates: r.candidates.map((c) => ({ id: c.id, posts: c.posts, scores: c.scores, total: c.total, note: c.note, changes: c.changes, warning: !!c.warning })), ...credits(r.quota) });
  }
  case 'format': {
    if (!['emojis', 'bullets'].includes(a.mode)) out({ ok: false, error: 'Pass --mode emojis or --mode bullets.' }, 1);
    const res = await call('/api/format', { method: 'POST', body: { posts: readPosts(a.posts), mode: a.mode } });
    const body = await res.json().catch(() => ({}));
    if (!res.ok) refused(res.status, body);
    out({ ok: true, posts: body.posts });
  }
  case 'post': {
    const res = await call('/api/post', { method: 'POST', body: { posts: readPosts(a.posts), craftId: a.craft, variantId: a.variant } });
    const body = await res.json().catch(() => ({}));
    if (!res.ok) {
      if (body.code === 'reconnect' || res.status === 403) needs('reconnect_x', `${body.error || 'X access expired.'} Sign in with X again at ${BASE}/ and retry.`);
      if (body.posted?.length) out({ ok: false, error: body.error, postedBeforeError: body.posted }, 1);
      refused(res.status, body);
    }
    out({ ok: true, posted: body.posted });
  }
  default:
    out({ ok: false, error: 'Commands: login <key> | me | craft --idea "…" | optimize --posts f.json | format --posts f.json --mode emojis|bullets | post --posts f.json' }, 1);
}
```

## File: README.md
upstream: https://github.com/lihaila/hitpost-skill/blob/main/README.md

````text
# HitPost skills for AI agents

Write X (Twitter) posts that earn attention and sound human, right inside your AI agent: Claude Code, Codex, Cursor, Gemini CLI or any agent that supports SKILL.md skills.

Two skills, from the makers of [HitPost](https://lihaila.si):

| | **hitpost** | **hitpost-pro** |
|---|---|---|
| What writes the posts | Your agent, following HitPost's writing rules | HitPost's own pipeline, on your account |
| Live X research (what X is saying this week) | – (web search only) | ✓ Grok reads X live |
| Polish with a blind multi-model judge | Your agent scores its own rewrites | ✓ DeepSeek + Qwen rewrite, Grok judges |
| X Articles | – | ✓ |
| Posting | Share links | ✓ Posts to your X account after you confirm |
| Cost | Free | Free to install; uses [HitPost credits](https://lihaila.si) (free credits on sign-up, then top up) |
| Needs | Nothing | A blue-check X account and a HitPost API key |

## hitpost (free)

Give it an idea, a topic, a link or a rough note. You get back **three ready-to-post drafts**, each with a different angle and a one-click **Post on X** link. It writes single posts, threads and long Premium posts, in any tone and in English, Chinese, Japanese, Korean and more. It uses the same writing rules HitPost runs in production.

- **Hooks first.** Every draft leads with a reader need (useful, money, insider, a surprising number, humor) and ends on a takeaway, not a restated headline.
- **Sounds human.** It keeps out the phrases that mark a post as AI-written ("game-changer", "Let's dive in", 🚨, "Excited to share", 赋能, 重磅, 家人们…).
- **Doesn't make things up.** Numbers, quotes and stories come only from your idea or real research. Research happens only when the post needs current facts, so opinions and tips don't turn into news recaps.
- **Counts length the way X does.** A bundled checker counts CJK characters and emoji as 2, so a single post shows in full without "Show more", and every post in a thread lands in range.
- **Polishes your drafts.** Paste your own tweet to get two rewrites, scored 1–10 on hook, clarity, value, shareability and how human it sounds.

## hitpost-pro (HitPost credits)

Runs the real HitPost on your account from inside your agent: live research, three drafts (or a full X Article), polish, then posting after you confirm the exact text.

1. Sign in with X at [lihaila.si](https://lihaila.si). HitPost is for blue-check accounts.
2. Open the account menu → **Use in AI agents** → **Create a key**.
3. Install the skill (below) and paste the key when your agent asks.

Each craft uses 1 credit; polishing and formatting are free within a daily allowance. New accounts get free credits once, then you top up on the site with Stripe. Credits are shared between the site and the skill, and a craft that fails is refunded. It needs an agent with network access; the claude.ai sandbox may block calls to outside servers.

## Install

**Claude Code**

```
/plugin marketplace add Lihaila/hitpost-skill
/plugin install hitpost@hitpost
```

This installs both skills. Or copy one in by hand:

```bash
git clone https://github.com/Lihaila/hitpost-skill
cp -r hitpost-skill/skills/hitpost ~/.claude/skills/        # free
cp -r hitpost-skill/skills/hitpost-pro ~/.claude/skills/    # HitPost credits
```

**Updating in Claude Code:** plugins don't update on their own. To get the latest version:

```
/plugin marketplace update hitpost
/plugin update hitpost@hitpost
```

Then restart Claude Code.

**Any other agent that supports SKILL.md skills** (Codex, Cursor, Gemini CLI and others): copy the skill folder from `skills/` into that agent's skills directory (see its docs for the location). The scripts are plain Node with no dependencies, so nothing else is needed. We built and tested these with Claude Code.

**claude.ai**

Download `hitpost.zip` (or `hitpost-pro.zip`) from the [latest release](https://github.com/Lihaila/hitpost-skill/releases/latest). Then open **Settings → Capabilities**, turn on code execution, and upload it under **Skills**.

Both skills need Node 18 or later for their scripts; they use the Node on your machine.

## Try it

```
write a tweet about this: cloudflare now blocks AI crawlers by default for new domains
帮我写一个推文串：独立开发者最大的问题不是技术，是不敢发布。语气犀利一点
make this tweet better: "Excited to share that I've been using Claude Code for 3 months…"
gm tweet pls, it's friday
write a thread on what X is saying about the new Grok model, then post it   # hitpost-pro
```

## License

MIT
````
