@spectratools/xapi-cli 0.2.4 → 0.3.0
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/README.md +7 -0
- package/dist/cli.js +121 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,10 +54,15 @@ xapi-cli trends location 1 --format json
|
|
|
54
54
|
xapi-cli posts search "AI agents" --sort relevancy --max-results 20 --format json
|
|
55
55
|
|
|
56
56
|
# 2) User intelligence pass
|
|
57
|
+
xapi-cli users me --format json
|
|
57
58
|
xapi-cli users get jack --format json
|
|
58
59
|
xapi-cli users posts jack --max-results 20 --format json
|
|
59
60
|
xapi-cli users followers jack --max-results 100 --format json
|
|
60
61
|
|
|
62
|
+
# 2b) Client-side follower delta (new followers since a known baseline)
|
|
63
|
+
# Baseline file format: one follower ID per line
|
|
64
|
+
xapi-cli users followers jack --new-only --seen-ids-file ./seen-followers.txt --format json
|
|
65
|
+
|
|
61
66
|
# 3) Moderation helper flow
|
|
62
67
|
xapi-cli posts get 1234567890 --format json
|
|
63
68
|
xapi-cli posts likes 1234567890 --max-results 100 --format json
|
|
@@ -75,5 +80,7 @@ xapi-cli dm send 12345 --text "hello from agent" --format json
|
|
|
75
80
|
## Notes
|
|
76
81
|
|
|
77
82
|
- All commands support JSON output with `--format json`.
|
|
83
|
+
- `users followers --new-only` performs **client-side diffing** against `--seen-ids-file`; it does not use an API-native `since_id` filter for follower deltas.
|
|
84
|
+
- Baseline files are read-only input (newline-delimited follower IDs) and are never mutated by the CLI.
|
|
78
85
|
- `X_BEARER_TOKEN` is for read-only app auth.
|
|
79
86
|
- `X_ACCESS_TOKEN` is required for write actions (`posts create`, `posts delete`, `dm send`).
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync, realpathSync } from "fs";
|
|
4
|
+
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
5
5
|
import { dirname, resolve } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { Cli as Cli7 } from "incur";
|
|
@@ -186,6 +186,12 @@ function createXApiClient(bearerToken) {
|
|
|
186
186
|
...nextToken ? { pagination_token: nextToken } : {}
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
|
+
function likePost(userId, tweetId) {
|
|
190
|
+
return post(`/users/${userId}/likes`, { tweet_id: tweetId });
|
|
191
|
+
}
|
|
192
|
+
function retweetPost(userId, tweetId) {
|
|
193
|
+
return post(`/users/${userId}/retweets`, { tweet_id: tweetId });
|
|
194
|
+
}
|
|
189
195
|
const USER_FIELDS = "id,name,username,description,public_metrics,created_at";
|
|
190
196
|
function getUserByUsername(username) {
|
|
191
197
|
return get(`/users/by/username/${username}`, {
|
|
@@ -211,6 +217,14 @@ function createXApiClient(bearerToken) {
|
|
|
211
217
|
...nextToken ? { pagination_token: nextToken } : {}
|
|
212
218
|
});
|
|
213
219
|
}
|
|
220
|
+
function followUser(sourceUserId, targetUserId) {
|
|
221
|
+
return post(`/users/${sourceUserId}/following`, {
|
|
222
|
+
target_user_id: targetUserId
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function unfollowUser(sourceUserId, targetUserId) {
|
|
226
|
+
return del(`/users/${sourceUserId}/following/${targetUserId}`);
|
|
227
|
+
}
|
|
214
228
|
function getUserPosts(id, maxResults, nextToken) {
|
|
215
229
|
return get(`/users/${id}/tweets`, {
|
|
216
230
|
max_results: maxResults,
|
|
@@ -231,18 +245,20 @@ function createXApiClient(bearerToken) {
|
|
|
231
245
|
"user.fields": USER_FIELDS
|
|
232
246
|
});
|
|
233
247
|
}
|
|
234
|
-
function getHomeTimeline(userId, maxResults, nextToken) {
|
|
248
|
+
function getHomeTimeline(userId, maxResults, nextToken, sinceId) {
|
|
235
249
|
return get(`/users/${userId}/timelines/reverse_chronological`, {
|
|
236
250
|
max_results: maxResults,
|
|
237
251
|
"tweet.fields": POST_FIELDS,
|
|
238
|
-
...nextToken ? { pagination_token: nextToken } : {}
|
|
252
|
+
...nextToken ? { pagination_token: nextToken } : {},
|
|
253
|
+
...sinceId ? { since_id: sinceId } : {}
|
|
239
254
|
});
|
|
240
255
|
}
|
|
241
|
-
function getMentionsTimeline(userId, maxResults, nextToken) {
|
|
256
|
+
function getMentionsTimeline(userId, maxResults, nextToken, sinceId) {
|
|
242
257
|
return get(`/users/${userId}/mentions`, {
|
|
243
258
|
max_results: maxResults,
|
|
244
259
|
"tweet.fields": POST_FIELDS,
|
|
245
|
-
...nextToken ? { pagination_token: nextToken } : {}
|
|
260
|
+
...nextToken ? { pagination_token: nextToken } : {},
|
|
261
|
+
...sinceId ? { since_id: sinceId } : {}
|
|
246
262
|
});
|
|
247
263
|
}
|
|
248
264
|
function getList(id) {
|
|
@@ -301,10 +317,14 @@ function createXApiClient(bearerToken) {
|
|
|
301
317
|
deletePost,
|
|
302
318
|
getPostLikes,
|
|
303
319
|
getPostRetweets,
|
|
320
|
+
likePost,
|
|
321
|
+
retweetPost,
|
|
304
322
|
getUserByUsername,
|
|
305
323
|
getUserById,
|
|
306
324
|
getUserFollowers,
|
|
307
325
|
getUserFollowing,
|
|
326
|
+
followUser,
|
|
327
|
+
unfollowUser,
|
|
308
328
|
getUserPosts,
|
|
309
329
|
getUserMentions,
|
|
310
330
|
searchUsers,
|
|
@@ -1045,6 +1065,7 @@ trends.command("location", {
|
|
|
1045
1065
|
});
|
|
1046
1066
|
|
|
1047
1067
|
// src/commands/users.ts
|
|
1068
|
+
import { readFileSync } from "fs";
|
|
1048
1069
|
import { Cli as Cli6, z as z7 } from "incur";
|
|
1049
1070
|
var users = Cli6.create("users", {
|
|
1050
1071
|
description: "Look up X users."
|
|
@@ -1055,6 +1076,79 @@ async function resolveUser(client, usernameOrId) {
|
|
|
1055
1076
|
}
|
|
1056
1077
|
return client.getUserByUsername(usernameOrId.replace(/^@/, ""));
|
|
1057
1078
|
}
|
|
1079
|
+
function formatUserProfile(user, verbose) {
|
|
1080
|
+
return {
|
|
1081
|
+
id: user.id,
|
|
1082
|
+
name: user.name,
|
|
1083
|
+
username: user.username,
|
|
1084
|
+
description: user.description ? verbose ? user.description : truncateText(user.description) : void 0,
|
|
1085
|
+
followers: user.public_metrics?.followers_count,
|
|
1086
|
+
following: user.public_metrics?.following_count,
|
|
1087
|
+
tweets: user.public_metrics?.tweet_count,
|
|
1088
|
+
joined: user.created_at ? relativeTime(user.created_at) : void 0
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function readSeenIds(filePath) {
|
|
1092
|
+
const fileContents = readFileSync(filePath, "utf8");
|
|
1093
|
+
return new Set(
|
|
1094
|
+
fileContents.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
var followersOptionsSchema = z7.object({
|
|
1098
|
+
maxResults: z7.number().default(100).describe("Maximum followers to return"),
|
|
1099
|
+
seenIdsFile: z7.string().optional().describe(
|
|
1100
|
+
"Path to newline-delimited follower IDs used as a baseline for client-side diffing"
|
|
1101
|
+
),
|
|
1102
|
+
newOnly: z7.boolean().default(false).describe(
|
|
1103
|
+
"Return only followers not found in --seen-ids-file (client-side baseline diff; not API-native since_id)"
|
|
1104
|
+
)
|
|
1105
|
+
}).refine((options) => !options.newOnly || Boolean(options.seenIdsFile), {
|
|
1106
|
+
path: ["seenIdsFile"],
|
|
1107
|
+
message: "--seen-ids-file is required when --new-only is set"
|
|
1108
|
+
});
|
|
1109
|
+
users.command("me", {
|
|
1110
|
+
description: "Get the authenticated user profile and metrics.",
|
|
1111
|
+
options: z7.object({
|
|
1112
|
+
verbose: z7.boolean().optional().describe("Show full bio without truncation")
|
|
1113
|
+
}),
|
|
1114
|
+
env: xApiReadEnv,
|
|
1115
|
+
output: z7.object({
|
|
1116
|
+
id: z7.string(),
|
|
1117
|
+
name: z7.string(),
|
|
1118
|
+
username: z7.string(),
|
|
1119
|
+
description: z7.string().optional(),
|
|
1120
|
+
followers: z7.number().optional(),
|
|
1121
|
+
following: z7.number().optional(),
|
|
1122
|
+
tweets: z7.number().optional(),
|
|
1123
|
+
joined: z7.string().optional()
|
|
1124
|
+
}),
|
|
1125
|
+
examples: [{ description: "Get your authenticated profile", options: {} }],
|
|
1126
|
+
async run(c) {
|
|
1127
|
+
const client = createXApiClient(readAuthToken(c.env));
|
|
1128
|
+
const res = await client.getMe();
|
|
1129
|
+
const user = res.data;
|
|
1130
|
+
return c.ok(
|
|
1131
|
+
formatUserProfile(user, c.options.verbose),
|
|
1132
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1133
|
+
cta: {
|
|
1134
|
+
description: "Explore your account:",
|
|
1135
|
+
commands: [
|
|
1136
|
+
{
|
|
1137
|
+
command: "users followers",
|
|
1138
|
+
args: { username: user.username },
|
|
1139
|
+
description: "View your followers"
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
command: "users posts",
|
|
1143
|
+
args: { username: user.username },
|
|
1144
|
+
description: "View your recent posts"
|
|
1145
|
+
}
|
|
1146
|
+
]
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1058
1152
|
users.command("get", {
|
|
1059
1153
|
description: "Get a user by username or ID.",
|
|
1060
1154
|
args: z7.object({
|
|
@@ -1083,16 +1177,7 @@ users.command("get", {
|
|
|
1083
1177
|
const res = await resolveUser(client, c.args.username);
|
|
1084
1178
|
const user = res.data;
|
|
1085
1179
|
return c.ok(
|
|
1086
|
-
|
|
1087
|
-
id: user.id,
|
|
1088
|
-
name: user.name,
|
|
1089
|
-
username: user.username,
|
|
1090
|
-
description: user.description ? c.options.verbose ? user.description : truncateText(user.description) : void 0,
|
|
1091
|
-
followers: user.public_metrics?.followers_count,
|
|
1092
|
-
following: user.public_metrics?.following_count,
|
|
1093
|
-
tweets: user.public_metrics?.tweet_count,
|
|
1094
|
-
joined: user.created_at ? relativeTime(user.created_at) : void 0
|
|
1095
|
-
},
|
|
1180
|
+
formatUserProfile(user, c.options.verbose),
|
|
1096
1181
|
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1097
1182
|
cta: {
|
|
1098
1183
|
description: "Explore this user:",
|
|
@@ -1114,14 +1199,12 @@ users.command("get", {
|
|
|
1114
1199
|
}
|
|
1115
1200
|
});
|
|
1116
1201
|
users.command("followers", {
|
|
1117
|
-
description: "List followers of a user.",
|
|
1202
|
+
description: "List followers of a user. Supports optional client-side baseline diffing for new follower detection.",
|
|
1118
1203
|
args: z7.object({
|
|
1119
1204
|
username: z7.string().describe("Username or user ID")
|
|
1120
1205
|
}),
|
|
1121
|
-
options:
|
|
1122
|
-
|
|
1123
|
-
}),
|
|
1124
|
-
alias: { maxResults: "n" },
|
|
1206
|
+
options: followersOptionsSchema,
|
|
1207
|
+
alias: { maxResults: "n", seenIdsFile: "s" },
|
|
1125
1208
|
env: xApiReadEnv,
|
|
1126
1209
|
output: z7.object({
|
|
1127
1210
|
users: z7.array(
|
|
@@ -1134,7 +1217,14 @@ users.command("followers", {
|
|
|
1134
1217
|
),
|
|
1135
1218
|
count: z7.number()
|
|
1136
1219
|
}),
|
|
1137
|
-
examples: [
|
|
1220
|
+
examples: [
|
|
1221
|
+
{ args: { username: "jack" }, description: "List followers of jack" },
|
|
1222
|
+
{
|
|
1223
|
+
args: { username: "jack" },
|
|
1224
|
+
options: { seenIdsFile: "./seen-followers.txt", newOnly: true },
|
|
1225
|
+
description: "Show only followers not in your baseline file (client-side diffing; the X API does not support since_id here)"
|
|
1226
|
+
}
|
|
1227
|
+
],
|
|
1138
1228
|
async run(c) {
|
|
1139
1229
|
const client = createXApiClient(readAuthToken(c.env));
|
|
1140
1230
|
const userRes = await resolveUser(client, c.args.username);
|
|
@@ -1150,7 +1240,15 @@ users.command("followers", {
|
|
|
1150
1240
|
c.options.maxResults,
|
|
1151
1241
|
1e3
|
|
1152
1242
|
);
|
|
1153
|
-
|
|
1243
|
+
if (!c.options.newOnly) {
|
|
1244
|
+
return c.ok({ users: allUsers, count: allUsers.length });
|
|
1245
|
+
}
|
|
1246
|
+
if (!c.options.seenIdsFile) {
|
|
1247
|
+
throw new Error("--seen-ids-file is required when --new-only is set");
|
|
1248
|
+
}
|
|
1249
|
+
const seenIds = readSeenIds(c.options.seenIdsFile);
|
|
1250
|
+
const newUsers = allUsers.filter((user) => !seenIds.has(user.id));
|
|
1251
|
+
return c.ok({ users: newUsers, count: newUsers.length });
|
|
1154
1252
|
}
|
|
1155
1253
|
});
|
|
1156
1254
|
users.command("following", {
|
|
@@ -1336,7 +1434,7 @@ users.command("search", {
|
|
|
1336
1434
|
|
|
1337
1435
|
// src/cli.ts
|
|
1338
1436
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1339
|
-
var pkg = JSON.parse(
|
|
1437
|
+
var pkg = JSON.parse(readFileSync2(resolve(__dirname, "../package.json"), "utf8"));
|
|
1340
1438
|
var cli = Cli7.create("xapi", {
|
|
1341
1439
|
version: pkg.version,
|
|
1342
1440
|
description: "X (Twitter) API CLI for spectra-the-bot."
|