92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
import { Type } from "@sinclair/typebox";
|
|
|
|
const BIRD_BIN = "/opt/homebrew/bin/bird";
|
|
|
|
const TweetParams = Type.Object({
|
|
url: Type.String({ minLength: 1, description: "Tweet URL or numeric ID" }),
|
|
mode: Type.Optional(
|
|
Type.Union([
|
|
Type.Literal("read"),
|
|
Type.Literal("thread"),
|
|
Type.Literal("replies"),
|
|
]),
|
|
),
|
|
});
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
pi.registerTool({
|
|
name: "read_tweet",
|
|
label: "Read Tweet",
|
|
description:
|
|
"Read an X/Twitter tweet or thread using the local bird CLI. " +
|
|
"Returns tweet author, text, media, metrics, and timestamps. " +
|
|
"Use this instead of webfetch for x.com/twitter.com URLs.",
|
|
promptSnippet: "Read X/Twitter tweets with bird CLI",
|
|
promptGuidelines: [
|
|
"Use read_tweet when the user shares an x.com or twitter.com URL and asks what it says",
|
|
"Use mode='thread' for full conversation threads",
|
|
"Use mode='replies' to fetch replies to a tweet",
|
|
"If fetch fails with auth errors, ask the user to sign in to x.com in their browser and retry",
|
|
],
|
|
parameters: TweetParams,
|
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
const { url, mode = "read" } = params;
|
|
|
|
try {
|
|
const { execSync } = await import("child_process");
|
|
const cmd = `${BIRD_BIN} ${mode} ${JSON.stringify(url)} --json`;
|
|
const stdout = execSync(cmd, { encoding: "utf-8", timeout: 15000 });
|
|
const parsed = JSON.parse(stdout);
|
|
|
|
const author = parsed.author?.name || parsed.author?.screen_name || "unknown";
|
|
const text = parsed.text || parsed.content || "";
|
|
const createdAt = parsed.created_at || "";
|
|
const retweetCount = parsed.metrics?.retweet_count ?? parsed.metrics?.retweets ?? 0;
|
|
const likeCount = parsed.metrics?.like_count ?? parsed.metrics?.likes ?? 0;
|
|
const replyCount = parsed.metrics?.reply_count ?? parsed.metrics?.replies ?? 0;
|
|
const mediaCount = parsed.media?.length ?? 0;
|
|
const threadCount = parsed.thread?.tweets?.length ?? 0;
|
|
const repliesCount = parsed.replies?.length ?? 0;
|
|
|
|
const lines: string[] = [];
|
|
lines.push(`Author: ${author}`);
|
|
if (createdAt) lines.push(`Posted: ${createdAt}`);
|
|
lines.push("");
|
|
lines.push(text);
|
|
if (retweetCount || likeCount || replyCount) {
|
|
lines.push("");
|
|
lines.push(`Retweets: ${retweetCount} Likes: ${likeCount} Replies: ${replyCount}`);
|
|
}
|
|
if (mediaCount) lines.push(`Media: ${mediaCount} attachment(s)`);
|
|
if (threadCount) lines.push(`Thread: ${threadCount} tweets total`);
|
|
if (repliesCount) lines.push(`Replies fetched: ${repliesCount}`);
|
|
|
|
return {
|
|
content: [
|
|
{ type: "text", text: lines.join("\n") },
|
|
{ type: "text", text: `\n--- raw ---\n${JSON.stringify(parsed, null, 2)}` },
|
|
],
|
|
details: {
|
|
author,
|
|
text: text.slice(0, 500),
|
|
url,
|
|
mode,
|
|
tweetCount: threadCount || repliesCount || 1,
|
|
},
|
|
};
|
|
} catch (e: any) {
|
|
const errMsg = e.stderr || e.message || String(e);
|
|
const hint = errMsg.includes("auth")
|
|
? " Sign in to x.com in your browser and retry."
|
|
: "";
|
|
|
|
return {
|
|
content: [{ type: "text", text: `Failed to fetch tweet: ${errMsg}${hint}` }],
|
|
details: { error: errMsg, url, mode },
|
|
};
|
|
}
|
|
},
|
|
});
|
|
}
|