@mulmoclaude/x-plugin 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +207 -0
- package/dist/index.cjs.map +1 -0
- package/package.json +2 -2
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/internal.ts
|
|
3
|
+
var ONE_SECOND_MS = 1e3;
|
|
4
|
+
/** Human-readable message from an unknown thrown value.
|
|
5
|
+
* Mirrors server/utils/errors.ts `errorMessage`. */
|
|
6
|
+
function errorMessage(err) {
|
|
7
|
+
if (err instanceof Error) return err.message;
|
|
8
|
+
if (err !== null && typeof err === "object") {
|
|
9
|
+
const obj = err;
|
|
10
|
+
if (typeof obj.details === "string" && obj.details) return obj.details;
|
|
11
|
+
if (typeof obj.message === "string" && obj.message) return obj.message;
|
|
12
|
+
}
|
|
13
|
+
return String(err);
|
|
14
|
+
}
|
|
15
|
+
/** Best-effort response body text, capped, never throwing.
|
|
16
|
+
* Mirrors server/utils/http.ts `safeResponseText`. */
|
|
17
|
+
async function safeResponseText(res, maxLength = 200) {
|
|
18
|
+
try {
|
|
19
|
+
return (await res.text()).slice(0, maxLength);
|
|
20
|
+
} catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** `Date` → `YYYY-MM-DD` in UTC. Mirrors server/utils/date.ts `toUtcIsoDate`. */
|
|
25
|
+
function toUtcIsoDate(timestamp) {
|
|
26
|
+
return `${timestamp.getUTCFullYear()}-${String(timestamp.getUTCMonth() + 1).padStart(2, "0")}-${String(timestamp.getUTCDate()).padStart(2, "0")}`;
|
|
27
|
+
}
|
|
28
|
+
/** `fetch` with a finite timeout that aborts the request once `timeoutMs`
|
|
29
|
+
* elapses. Compact port of server/utils/fetch.ts `fetchWithTimeout` — the X
|
|
30
|
+
* tools never pass a caller signal, so the external-signal bridging is omitted. */
|
|
31
|
+
async function fetchWithTimeout(url, init = {}) {
|
|
32
|
+
const { timeoutMs = 10 * ONE_SECOND_MS, ...rest } = init;
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, "TimeoutError"));
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(url, {
|
|
39
|
+
...rest,
|
|
40
|
+
signal: controller.signal
|
|
41
|
+
});
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/client.ts
|
|
48
|
+
var X_API_BASE = "https://api.twitter.com/2";
|
|
49
|
+
var X_API_TIMEOUT_MS = 20 * ONE_SECOND_MS;
|
|
50
|
+
var TWEET_FIELDS = "tweet.fields=created_at,author_id,public_metrics,entities,note_tweet,article";
|
|
51
|
+
var EXPANSIONS = "expansions=author_id";
|
|
52
|
+
var USER_FIELDS = "user.fields=name,username";
|
|
53
|
+
/** Resolve the X API bearer token from the environment. The host gates the
|
|
54
|
+
* tools on `requiredEnv: ["X_BEARER_TOKEN"]` before dispatch, but the body
|
|
55
|
+
* re-checks so direct/test callers get a clear error. */
|
|
56
|
+
function xBearerToken() {
|
|
57
|
+
return process.env.X_BEARER_TOKEN;
|
|
58
|
+
}
|
|
59
|
+
async function fetchX(path) {
|
|
60
|
+
const token = xBearerToken();
|
|
61
|
+
if (!token) throw new Error("X_BEARER_TOKEN is not configured in .env");
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await fetchWithTimeout(`${X_API_BASE}${path}`, {
|
|
65
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
66
|
+
timeoutMs: X_API_TIMEOUT_MS
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
throw new Error(`Network error calling X API: ${errorMessage(err)}`);
|
|
70
|
+
}
|
|
71
|
+
if (response.status === 401) throw new Error("X API error 401: Invalid or expired Bearer Token.");
|
|
72
|
+
if (response.status === 429) throw new Error("X API error 429: Rate limit exceeded. Please wait before retrying.");
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const body = await safeResponseText(response);
|
|
75
|
+
throw new Error(`X API error ${response.status}: ${body}`);
|
|
76
|
+
}
|
|
77
|
+
return response.json();
|
|
78
|
+
}
|
|
79
|
+
function tweetBody(tweet) {
|
|
80
|
+
if (tweet.note_tweet?.text) return tweet.note_tweet.text;
|
|
81
|
+
const { article } = tweet;
|
|
82
|
+
if (article?.plain_text) return [article.title, article.plain_text].filter(Boolean).join("\n\n");
|
|
83
|
+
return tweet.text;
|
|
84
|
+
}
|
|
85
|
+
function formatTweet(tweet, author, url) {
|
|
86
|
+
const date = tweet.created_at ? toUtcIsoDate(new Date(tweet.created_at)) : "";
|
|
87
|
+
const dateSuffix = date ? ` · ${date}` : "";
|
|
88
|
+
const byline = author ? `@${author.username} (${author.name})${dateSuffix}` : date;
|
|
89
|
+
const metrics = tweet.public_metrics ? `Likes: ${tweet.public_metrics.like_count} | Retweets: ${tweet.public_metrics.retweet_count} | Replies: ${tweet.public_metrics.reply_count}` : "";
|
|
90
|
+
const link = url ?? "";
|
|
91
|
+
return [
|
|
92
|
+
byline,
|
|
93
|
+
"",
|
|
94
|
+
tweetBody(tweet),
|
|
95
|
+
"",
|
|
96
|
+
metrics,
|
|
97
|
+
link
|
|
98
|
+
].filter((line) => line !== void 0).join("\n").trimEnd();
|
|
99
|
+
}
|
|
100
|
+
/** Extract a numeric tweet id from a full x.com/twitter.com status URL or a
|
|
101
|
+
* bare id. Returns null when neither form matches. */
|
|
102
|
+
function extractTweetId(url) {
|
|
103
|
+
const match = url.match(/status\/(\d+)/);
|
|
104
|
+
if (match) return match[1];
|
|
105
|
+
return /^\d+$/.test(url) ? url : null;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/index.ts
|
|
109
|
+
var readXPost = {
|
|
110
|
+
definition: {
|
|
111
|
+
name: "readXPost",
|
|
112
|
+
description: "Fetch the content of a single X (Twitter) post by URL or tweet ID. Returns the author, text, and engagement metrics.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: { url: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "Full X post URL (https://x.com/user/status/ID) or bare tweet ID."
|
|
118
|
+
} },
|
|
119
|
+
required: ["url"]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
requiredEnv: ["X_BEARER_TOKEN"],
|
|
123
|
+
prompt: "Use the readXPost tool whenever the user shares a URL from x.com or twitter.com.",
|
|
124
|
+
async handler(args) {
|
|
125
|
+
const url = String(args.url ?? "");
|
|
126
|
+
const tweetId = extractTweetId(url);
|
|
127
|
+
if (!tweetId) return `Could not extract a tweet ID from: ${url}. Provide a full x.com URL or a numeric tweet ID.`;
|
|
128
|
+
let data;
|
|
129
|
+
try {
|
|
130
|
+
data = await fetchX(`/tweets/${tweetId}?${TWEET_FIELDS}&${EXPANSIONS}&${USER_FIELDS}`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return errorMessage(err);
|
|
133
|
+
}
|
|
134
|
+
if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
|
|
135
|
+
const tweet = data.data;
|
|
136
|
+
if (!tweet) return "Tweet not found.";
|
|
137
|
+
const author = data.includes?.users?.find((user) => user.id === tweet.author_id);
|
|
138
|
+
return formatTweet(tweet, author, author ? `https://x.com/${author.username}/status/${tweet.id}` : void 0);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var searchX = {
|
|
142
|
+
definition: {
|
|
143
|
+
name: "searchX",
|
|
144
|
+
description: "Search recent X (Twitter) posts by keyword or query. Returns up to max_results posts (default 10, max 100).",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
query: {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "X search query. Supports operators like from:user, #hashtag, -excludeword."
|
|
151
|
+
},
|
|
152
|
+
max_results: {
|
|
153
|
+
type: "number",
|
|
154
|
+
description: "Number of results to return (10–100). Defaults to 10."
|
|
155
|
+
},
|
|
156
|
+
sort_order: {
|
|
157
|
+
type: "string",
|
|
158
|
+
enum: ["recency", "relevancy"],
|
|
159
|
+
description: "'recency' = latest tweets first (default). 'relevancy' = most relevant (Top) first."
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
required: ["query"]
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
requiredEnv: ["X_BEARER_TOKEN"],
|
|
166
|
+
prompt: "Use the searchX tool to find recent posts on X by keyword or topic.",
|
|
167
|
+
async handler(args) {
|
|
168
|
+
const query = String(args.query ?? "").trim();
|
|
169
|
+
if (!query) return "A search query is required.";
|
|
170
|
+
const maxResults = Math.min(100, Math.max(10, Number(args.max_results ?? 10)));
|
|
171
|
+
let data;
|
|
172
|
+
try {
|
|
173
|
+
const sortOrder = args.sort_order === "relevancy" ? "relevancy" : "recency";
|
|
174
|
+
const params = new URLSearchParams({
|
|
175
|
+
query,
|
|
176
|
+
max_results: String(maxResults),
|
|
177
|
+
sort_order: sortOrder
|
|
178
|
+
});
|
|
179
|
+
params.append("tweet.fields", "created_at,author_id,public_metrics");
|
|
180
|
+
params.append("expansions", "author_id");
|
|
181
|
+
params.append("user.fields", "name,username");
|
|
182
|
+
data = await fetchX(`/tweets/search/recent?${params.toString()}`);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return errorMessage(err);
|
|
185
|
+
}
|
|
186
|
+
if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
|
|
187
|
+
const tweets = Array.isArray(data.data) ? data.data : [];
|
|
188
|
+
if (tweets.length === 0) return `No recent posts found for: "${query}"`;
|
|
189
|
+
const users = data.includes?.users ?? [];
|
|
190
|
+
const userMap = new Map(users.map((user) => [user.id, user]));
|
|
191
|
+
const lines = [`Search: "${query}" — ${tweets.length} result${tweets.length !== 1 ? "s" : ""}`, ""];
|
|
192
|
+
tweets.forEach((tweet, i) => {
|
|
193
|
+
const author = tweet.author_id ? userMap.get(tweet.author_id) : void 0;
|
|
194
|
+
lines.push(`${i + 1}. ${formatTweet(tweet, author)}`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
});
|
|
197
|
+
return lines.join("\n").trimEnd();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
//#endregion
|
|
201
|
+
exports.extractTweetId = extractTweetId;
|
|
202
|
+
exports.formatTweet = formatTweet;
|
|
203
|
+
exports.readXPost = readXPost;
|
|
204
|
+
exports.searchX = searchX;
|
|
205
|
+
exports.tweetBody = tweetBody;
|
|
206
|
+
|
|
207
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/internal.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["// Self-contained ports of the few host utilities the X tools relied on,\n// inlined so the package carries no dependency on MulmoClaude's server tree\n// (server/utils/{errors,fetch,http,date,time}). Kept faithful to the\n// originals; see the matching files in the host repo for rationale.\n\nexport const ONE_SECOND_MS = 1_000;\n\n/** Human-readable message from an unknown thrown value.\n * Mirrors server/utils/errors.ts `errorMessage`. */\nexport function errorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (err !== null && typeof err === \"object\") {\n const obj = err as { details?: unknown; message?: unknown };\n if (typeof obj.details === \"string\" && obj.details) return obj.details;\n if (typeof obj.message === \"string\" && obj.message) return obj.message;\n }\n return String(err);\n}\n\n/** Best-effort response body text, capped, never throwing.\n * Mirrors server/utils/http.ts `safeResponseText`. */\nexport async function safeResponseText(res: Response, maxLength = 200): Promise<string> {\n try {\n const text = await res.text();\n return text.slice(0, maxLength);\n } catch {\n return \"\";\n }\n}\n\n/** `Date` → `YYYY-MM-DD` in UTC. Mirrors server/utils/date.ts `toUtcIsoDate`. */\nexport function toUtcIsoDate(timestamp: Date): string {\n const year = timestamp.getUTCFullYear();\n const month = String(timestamp.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(timestamp.getUTCDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/** `fetch` with a finite timeout that aborts the request once `timeoutMs`\n * elapses. Compact port of server/utils/fetch.ts `fetchWithTimeout` — the X\n * tools never pass a caller signal, so the external-signal bridging is omitted. */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = 10 * ONE_SECOND_MS, ...rest } = init;\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n}\n","import { errorMessage, fetchWithTimeout, ONE_SECOND_MS, safeResponseText, toUtcIsoDate } from \"./internal\";\n\nconst X_API_BASE = \"https://api.twitter.com/2\";\n\n// X API can stall under rate limit — a 10 s default (used for internal\n// localhost calls) would produce false timeouts. 20 s gives enough\n// headroom for a slow but real response while still bailing long\n// before the MCP client's tool-call timeout fires.\nexport const X_API_TIMEOUT_MS = 20 * ONE_SECOND_MS;\n\nexport const TWEET_FIELDS = \"tweet.fields=created_at,author_id,public_metrics,entities,note_tweet,article\";\nexport const EXPANSIONS = \"expansions=author_id\";\nexport const USER_FIELDS = \"user.fields=name,username\";\n\nexport interface XUser {\n id: string;\n name: string;\n username: string;\n}\n\nexport interface XTweet {\n id: string;\n text: string;\n author_id?: string;\n created_at?: string;\n // Long-form Post (>280 chars): full body lives here, not in `text`.\n note_tweet?: { text: string };\n // X Article (rich long-form, up to 100k chars): `text` only holds the t.co\n // link, so the body must be read from `article.plain_text`.\n article?: { title?: string; plain_text?: string };\n public_metrics?: {\n like_count: number;\n retweet_count: number;\n reply_count: number;\n };\n}\n\nexport interface XApiResponse {\n data?: XTweet | XTweet[];\n includes?: { users?: XUser[] };\n errors?: { detail: string }[];\n meta?: { result_count: number };\n}\n\n/** Resolve the X API bearer token from the environment. The host gates the\n * tools on `requiredEnv: [\"X_BEARER_TOKEN\"]` before dispatch, but the body\n * re-checks so direct/test callers get a clear error. */\nexport function xBearerToken(): string | undefined {\n return process.env.X_BEARER_TOKEN;\n}\n\nexport async function fetchX(path: string): Promise<XApiResponse> {\n const token = xBearerToken();\n if (!token) throw new Error(\"X_BEARER_TOKEN is not configured in .env\");\n\n let response: Response;\n try {\n response = await fetchWithTimeout(`${X_API_BASE}${path}`, {\n headers: { Authorization: `Bearer ${token}` },\n timeoutMs: X_API_TIMEOUT_MS,\n });\n } catch (err) {\n throw new Error(`Network error calling X API: ${errorMessage(err)}`);\n }\n\n if (response.status === 401) throw new Error(\"X API error 401: Invalid or expired Bearer Token.\");\n if (response.status === 429) throw new Error(\"X API error 429: Rate limit exceeded. Please wait before retrying.\");\n if (!response.ok) {\n const body = await safeResponseText(response);\n throw new Error(`X API error ${response.status}: ${body}`);\n }\n\n return response.json() as Promise<XApiResponse>;\n}\n\n// `text` caps at 280 chars; long-form Posts and Articles carry their real body\n// in `note_tweet` / `article`. Prefer those so the LLM sees the full content.\nexport function tweetBody(tweet: XTweet): string {\n if (tweet.note_tweet?.text) return tweet.note_tweet.text;\n const { article } = tweet;\n if (article?.plain_text) {\n return [article.title, article.plain_text].filter(Boolean).join(\"\\n\\n\");\n }\n return tweet.text;\n}\n\nexport function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {\n const date = tweet.created_at ? toUtcIsoDate(new Date(tweet.created_at)) : \"\";\n const dateSuffix = date ? ` · ${date}` : \"\";\n const byline = author ? `@${author.username} (${author.name})${dateSuffix}` : date;\n const metrics = tweet.public_metrics\n ? `Likes: ${tweet.public_metrics.like_count} | Retweets: ${tweet.public_metrics.retweet_count} | Replies: ${tweet.public_metrics.reply_count}`\n : \"\";\n const link = url ?? \"\";\n return [byline, \"\", tweetBody(tweet), \"\", metrics, link]\n .filter((line) => line !== undefined)\n .join(\"\\n\")\n .trimEnd();\n}\n\n/** Extract a numeric tweet id from a full x.com/twitter.com status URL or a\n * bare id. Returns null when neither form matches. */\nexport function extractTweetId(url: string): string | null {\n const match = url.match(/status\\/(\\d+)/);\n if (match) return match[1];\n return /^\\d+$/.test(url) ? url : null;\n}\n","// @mulmoclaude/x-plugin — X (Twitter) API tools.\n//\n// Two server-only MCP tools (no Vue View): `readXPost` and `searchX`.\n// Shared by MulmoClaude and MulmoTerminal so the X integration isn't\n// duplicated. Each host imports these objects and slots them into its own\n// MCP tool registry; the host gates them on `requiredEnv` and supplies the\n// `X_BEARER_TOKEN` env var. All formatting/fetch logic lives here.\n\nimport { errorMessage } from \"./internal\";\nimport { EXPANSIONS, extractTweetId, fetchX, formatTweet, TWEET_FIELDS, USER_FIELDS, type XApiResponse, type XTweet, type XUser } from \"./client\";\n\n/** Minimal MCP-tool shape these tools conform to. Structurally compatible\n * with the host's `McpTool` interface (server/agent/mcp-tools/index.ts), so\n * a host can drop these straight into its `McpTool[]` registry. */\nexport interface XTool {\n definition: {\n name: string;\n description: string;\n inputSchema: object;\n };\n requiredEnv: string[];\n prompt: string;\n handler: (args: Record<string, unknown>) => Promise<string>;\n}\n\nexport const readXPost: XTool = {\n definition: {\n name: \"readXPost\",\n description: \"Fetch the content of a single X (Twitter) post by URL or tweet ID. Returns the author, text, and engagement metrics.\",\n inputSchema: {\n type: \"object\",\n properties: {\n url: {\n type: \"string\",\n description: \"Full X post URL (https://x.com/user/status/ID) or bare tweet ID.\",\n },\n },\n required: [\"url\"],\n },\n },\n\n requiredEnv: [\"X_BEARER_TOKEN\"],\n\n prompt: \"Use the readXPost tool whenever the user shares a URL from x.com or twitter.com.\",\n\n async handler(args: Record<string, unknown>): Promise<string> {\n const url = String(args.url ?? \"\");\n const tweetId = extractTweetId(url);\n if (!tweetId) return `Could not extract a tweet ID from: ${url}. Provide a full x.com URL or a numeric tweet ID.`;\n\n let data: XApiResponse;\n try {\n data = await fetchX(`/tweets/${tweetId}?${TWEET_FIELDS}&${EXPANSIONS}&${USER_FIELDS}`);\n } catch (err) {\n return errorMessage(err);\n }\n\n if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join(\"; \")}`;\n\n const tweet = data.data as XTweet | undefined;\n if (!tweet) return \"Tweet not found.\";\n\n const author = data.includes?.users?.find((user) => user.id === tweet.author_id);\n const canonicalUrl = author ? `https://x.com/${author.username}/status/${tweet.id}` : undefined;\n return formatTweet(tweet, author, canonicalUrl);\n },\n};\n\nexport const searchX: XTool = {\n definition: {\n name: \"searchX\",\n description: \"Search recent X (Twitter) posts by keyword or query. Returns up to max_results posts (default 10, max 100).\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: {\n type: \"string\",\n description: \"X search query. Supports operators like from:user, #hashtag, -excludeword.\",\n },\n max_results: {\n type: \"number\",\n description: \"Number of results to return (10–100). Defaults to 10.\",\n },\n sort_order: {\n type: \"string\",\n enum: [\"recency\", \"relevancy\"],\n description: \"'recency' = latest tweets first (default). 'relevancy' = most relevant (Top) first.\",\n },\n },\n required: [\"query\"],\n },\n },\n\n requiredEnv: [\"X_BEARER_TOKEN\"],\n\n prompt: \"Use the searchX tool to find recent posts on X by keyword or topic.\",\n\n async handler(args: Record<string, unknown>): Promise<string> {\n const query = String(args.query ?? \"\").trim();\n if (!query) return \"A search query is required.\";\n\n const maxResults = Math.min(100, Math.max(10, Number(args.max_results ?? 10)));\n\n let data: XApiResponse;\n try {\n const sortOrder = args.sort_order === \"relevancy\" ? \"relevancy\" : \"recency\";\n const params = new URLSearchParams({\n query,\n max_results: String(maxResults),\n sort_order: sortOrder,\n });\n params.append(\"tweet.fields\", \"created_at,author_id,public_metrics\");\n params.append(\"expansions\", \"author_id\");\n params.append(\"user.fields\", \"name,username\");\n data = await fetchX(`/tweets/search/recent?${params.toString()}`);\n } catch (err) {\n return errorMessage(err);\n }\n\n if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join(\"; \")}`;\n\n const tweets = Array.isArray(data.data) ? data.data : [];\n if (tweets.length === 0) return `No recent posts found for: \"${query}\"`;\n\n const users = data.includes?.users ?? [];\n const userMap = new Map<string, XUser>(users.map((user) => [user.id, user]));\n\n const lines: string[] = [`Search: \"${query}\" — ${tweets.length} result${tweets.length !== 1 ? \"s\" : \"\"}`, \"\"];\n tweets.forEach((tweet, i) => {\n const author = tweet.author_id ? userMap.get(tweet.author_id) : undefined;\n lines.push(`${i + 1}. ${formatTweet(tweet, author)}`);\n lines.push(\"\");\n });\n\n return lines.join(\"\\n\").trimEnd();\n },\n};\n\nexport type { XApiResponse, XTweet, XUser } from \"./client\";\nexport { extractTweetId, formatTweet, tweetBody } from \"./client\";\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmoclaude/x-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "X (Twitter) API tools for MulmoClaude and MulmoTerminal — readXPost + searchX as MCP tools. Server-only; no Vue View.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
|
-
"require": "./dist/index.
|
|
13
|
+
"require": "./dist/index.cjs",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
}
|
|
16
16
|
},
|