@dotta/xc 0.1.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 +348 -0
- package/dist/__tests__/bookmarks.test.d.ts +4 -0
- package/dist/__tests__/bookmarks.test.js +104 -0
- package/dist/__tests__/bookmarks.test.js.map +1 -0
- package/dist/__tests__/budget.test.d.ts +6 -0
- package/dist/__tests__/budget.test.js +105 -0
- package/dist/__tests__/budget.test.js.map +1 -0
- package/dist/__tests__/dm.test.d.ts +4 -0
- package/dist/__tests__/dm.test.js +115 -0
- package/dist/__tests__/dm.test.js.map +1 -0
- package/dist/__tests__/followers.test.d.ts +4 -0
- package/dist/__tests__/followers.test.js +129 -0
- package/dist/__tests__/followers.test.js.map +1 -0
- package/dist/__tests__/lib/api.test.d.ts +5 -0
- package/dist/__tests__/lib/api.test.js +202 -0
- package/dist/__tests__/lib/api.test.js.map +1 -0
- package/dist/__tests__/lib/budget.test.d.ts +5 -0
- package/dist/__tests__/lib/budget.test.js +194 -0
- package/dist/__tests__/lib/budget.test.js.map +1 -0
- package/dist/__tests__/lib/config.test.d.ts +6 -0
- package/dist/__tests__/lib/config.test.js +228 -0
- package/dist/__tests__/lib/config.test.js.map +1 -0
- package/dist/__tests__/lib/cost.test.d.ts +6 -0
- package/dist/__tests__/lib/cost.test.js +177 -0
- package/dist/__tests__/lib/cost.test.js.map +1 -0
- package/dist/__tests__/lib/format.test.d.ts +4 -0
- package/dist/__tests__/lib/format.test.js +139 -0
- package/dist/__tests__/lib/format.test.js.map +1 -0
- package/dist/__tests__/lib/oauth.test.d.ts +5 -0
- package/dist/__tests__/lib/oauth.test.js +123 -0
- package/dist/__tests__/lib/oauth.test.js.map +1 -0
- package/dist/__tests__/lib/resolve.test.d.ts +4 -0
- package/dist/__tests__/lib/resolve.test.js +154 -0
- package/dist/__tests__/lib/resolve.test.js.map +1 -0
- package/dist/__tests__/lists.test.d.ts +4 -0
- package/dist/__tests__/lists.test.js +96 -0
- package/dist/__tests__/lists.test.js.map +1 -0
- package/dist/__tests__/media.test.d.ts +4 -0
- package/dist/__tests__/media.test.js +132 -0
- package/dist/__tests__/media.test.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +93 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +191 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/block.d.ts +15 -0
- package/dist/commands/block.js +117 -0
- package/dist/commands/block.js.map +1 -0
- package/dist/commands/bookmarks.d.ts +12 -0
- package/dist/commands/bookmarks.js +100 -0
- package/dist/commands/bookmarks.js.map +1 -0
- package/dist/commands/budget.d.ts +9 -0
- package/dist/commands/budget.js +124 -0
- package/dist/commands/budget.js.map +1 -0
- package/dist/commands/cost.d.ts +5 -0
- package/dist/commands/cost.js +75 -0
- package/dist/commands/cost.js.map +1 -0
- package/dist/commands/delete.d.ts +8 -0
- package/dist/commands/delete.js +31 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +179 -0
- package/dist/commands/dm.js.map +1 -0
- package/dist/commands/engagement.d.ts +14 -0
- package/dist/commands/engagement.js +167 -0
- package/dist/commands/engagement.js.map +1 -0
- package/dist/commands/followers.d.ts +14 -0
- package/dist/commands/followers.js +138 -0
- package/dist/commands/followers.js.map +1 -0
- package/dist/commands/get.d.ts +2 -0
- package/dist/commands/get.js +63 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/hide.d.ts +10 -0
- package/dist/commands/hide.js +58 -0
- package/dist/commands/hide.js.map +1 -0
- package/dist/commands/like.d.ts +3 -0
- package/dist/commands/like.js +52 -0
- package/dist/commands/like.js.map +1 -0
- package/dist/commands/lists.d.ts +20 -0
- package/dist/commands/lists.js +384 -0
- package/dist/commands/lists.js.map +1 -0
- package/dist/commands/media.d.ts +19 -0
- package/dist/commands/media.js +205 -0
- package/dist/commands/media.js.map +1 -0
- package/dist/commands/mentions.d.ts +8 -0
- package/dist/commands/mentions.js +59 -0
- package/dist/commands/mentions.js.map +1 -0
- package/dist/commands/mute.d.ts +12 -0
- package/dist/commands/mute.js +99 -0
- package/dist/commands/mute.js.map +1 -0
- package/dist/commands/post.d.ts +11 -0
- package/dist/commands/post.js +87 -0
- package/dist/commands/post.js.map +1 -0
- package/dist/commands/repost.d.ts +10 -0
- package/dist/commands/repost.js +59 -0
- package/dist/commands/repost.js.map +1 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +49 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/stream.d.ts +13 -0
- package/dist/commands/stream.js +251 -0
- package/dist/commands/stream.js.map +1 -0
- package/dist/commands/timeline.d.ts +2 -0
- package/dist/commands/timeline.js +61 -0
- package/dist/commands/timeline.js.map +1 -0
- package/dist/commands/trends.d.ts +10 -0
- package/dist/commands/trends.js +59 -0
- package/dist/commands/trends.js.map +1 -0
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +52 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/user.d.ts +2 -0
- package/dist/commands/user.js +43 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/commands/usersearch.d.ts +8 -0
- package/dist/commands/usersearch.js +48 -0
- package/dist/commands/usersearch.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +54 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/lib/api.d.ts +12 -0
- package/dist/lib/api.js +91 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/budget.d.ts +44 -0
- package/dist/lib/budget.js +119 -0
- package/dist/lib/budget.js.map +1 -0
- package/dist/lib/config.d.ts +39 -0
- package/dist/lib/config.js +63 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/cost.d.ts +43 -0
- package/dist/lib/cost.js +224 -0
- package/dist/lib/cost.js.map +1 -0
- package/dist/lib/format.d.ts +24 -0
- package/dist/lib/format.js +72 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/oauth.d.ts +32 -0
- package/dist/lib/oauth.js +132 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/lib/resolve.d.ts +12 -0
- package/dist/lib/resolve.js +48 -0
- package/dist/lib/resolve.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# xc — X API CLI
|
|
2
|
+
|
|
3
|
+
CLI client for the [X API v2](https://docs.x.com/x-api/introduction). Pay-per-use, no cookie scraping. Built on the official [@xdevplatform/xdk](https://github.com/xdevplatform/xdk) SDK with OAuth 2.0 PKCE.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @dotta/xc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js >= 18.
|
|
12
|
+
|
|
13
|
+
## Agent Skill
|
|
14
|
+
|
|
15
|
+
xc includes an [agent skill](skills/xc-cli/SKILL.md) so your agent can use `xc` on your behalf.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx skills add https://github.com/cryppadotta/xc --skill xc-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
Get a Bearer Token from [console.x.com](https://console.x.com) and start reading immediately:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
xc auth token <BEARER_TOKEN>
|
|
27
|
+
xc get https://x.com/dotta/status/1612500057768755201
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Bearer tokens are read-only. For posting, liking, following, and other write operations, see [Full OAuth Setup](#full-oauth-setup) below.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### Reading
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
xc get <post-id> # Get a post by ID
|
|
38
|
+
xc get https://x.com/dotta/status/1612500057768755201 # Or by URL
|
|
39
|
+
xc search "typescript" # Search recent posts (last 7 days)
|
|
40
|
+
xc search "from:dotta" -n 20 # Search by author
|
|
41
|
+
xc search "AI" --archive # Full archive search (if your plan supports it)
|
|
42
|
+
xc user dotta # Look up a user by @username
|
|
43
|
+
xc usersearch "keyword" # Search for users by keyword
|
|
44
|
+
xc timeline # Your home timeline
|
|
45
|
+
xc timeline dotta # A specific user's posts
|
|
46
|
+
xc mentions # Your mentions
|
|
47
|
+
xc mentions dotta # Another user's mentions
|
|
48
|
+
xc whoami # Show authenticated user
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Posting
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
xc post "Hello world" # Create a post
|
|
55
|
+
xc post "Reply" --reply 123456 # Reply to a post
|
|
56
|
+
xc post "Check this" --quote 123 # Quote a post
|
|
57
|
+
xc post "First" --thread "Second" "Third" # Post a thread
|
|
58
|
+
xc post "Photo" --media photo.jpg # Post with media attachment
|
|
59
|
+
xc post "text" --json # Show raw response
|
|
60
|
+
xc delete 1234567890 # Delete a post
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Likes & Reposts
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
xc like 1234567890 # Like a post by ID
|
|
67
|
+
xc unlike 1234567890 # Unlike a post
|
|
68
|
+
xc repost 1234567890 # Repost a post
|
|
69
|
+
xc unrepost 1234567890 # Undo a repost
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Engagement Lookups
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
xc quotes 1234567890 # List quote tweets of a post
|
|
76
|
+
xc likes 1234567890 # List users who liked a post
|
|
77
|
+
xc reposts 1234567890 # List users who reposted a post
|
|
78
|
+
xc liked # Posts you've liked
|
|
79
|
+
xc liked username # Posts liked by a user
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Reply Moderation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
xc hide 1234567890 # Hide a reply on your post
|
|
86
|
+
xc unhide 1234567890 # Unhide a reply
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Bookmarks
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
xc bookmarks # List your bookmarks
|
|
93
|
+
xc bookmark 1234567890 # Bookmark a post
|
|
94
|
+
xc unbookmark 1234567890 # Remove bookmark
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Blocks & Mutes
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
xc block username # Block a user
|
|
101
|
+
xc unblock username # Unblock a user
|
|
102
|
+
xc blocked # List blocked users
|
|
103
|
+
xc blocked --json # JSON output
|
|
104
|
+
xc mute username # Mute a user
|
|
105
|
+
xc unmute username # Unmute a user
|
|
106
|
+
xc muted # List muted users
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Lists
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
xc lists # List your owned lists
|
|
113
|
+
xc list view 1234567890 # View posts in a list
|
|
114
|
+
xc list create "My List" # Create a new list
|
|
115
|
+
xc list create "Secret" --private --description "My private list"
|
|
116
|
+
xc list update 123 --name "New Name" --description "Updated"
|
|
117
|
+
xc list update 123 --public # Make public
|
|
118
|
+
xc list delete 1234567890 # Delete a list
|
|
119
|
+
xc list members 1234567890 # List members
|
|
120
|
+
xc list add 123 username # Add a member
|
|
121
|
+
xc list remove 123 username # Remove a member
|
|
122
|
+
xc list follow 1234567890 # Follow a list
|
|
123
|
+
xc list unfollow 1234567890 # Unfollow a list
|
|
124
|
+
xc list pin 1234567890 # Pin a list
|
|
125
|
+
xc list unpin 1234567890 # Unpin a list
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Followers
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
xc followers dotta # List followers of a user
|
|
132
|
+
xc followers dotta --limit 50
|
|
133
|
+
xc following dotta # List who a user follows
|
|
134
|
+
xc follow dotta # Follow a user
|
|
135
|
+
xc unfollow dotta # Unfollow a user
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Trends
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
xc trends # Personalized trending topics
|
|
142
|
+
xc trends --global # Worldwide trends
|
|
143
|
+
xc trends 2459115 # Trends by location (WOEID)
|
|
144
|
+
xc trends --json # JSON output
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Direct Messages
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
xc dm list # List recent DM conversations
|
|
151
|
+
xc dm history username # View DM history with a user
|
|
152
|
+
xc dm send username "Hello" # Send a DM
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Media
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
xc media upload photo.jpg # Upload media, returns media_id
|
|
159
|
+
# Then use with post:
|
|
160
|
+
xc post "Check this out" --media photo.jpg
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Streaming
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
xc stream rules # List current stream rules
|
|
167
|
+
xc stream add "AI OR LLM" # Add a filtered stream rule
|
|
168
|
+
xc stream remove <rule-id> # Remove a rule by ID
|
|
169
|
+
xc stream clear # Remove all rules
|
|
170
|
+
xc stream connect # Connect to stream (outputs posts in real-time)
|
|
171
|
+
xc stream connect --json # Raw JSON stream output
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Full OAuth Setup
|
|
175
|
+
|
|
176
|
+
For write operations (posting, liking, following, DMs, etc.), you need OAuth 2.0 with PKCE.
|
|
177
|
+
|
|
178
|
+
### Getting a Client ID
|
|
179
|
+
|
|
180
|
+
1. Go to [developer.x.com](https://developer.x.com) (existing apps) or [console.x.com](https://console.x.com) (new projects)
|
|
181
|
+
2. Create or select an app
|
|
182
|
+
3. Under **OAuth 2.0 settings**, copy your **Client ID** (and optionally your **Client Secret** for confidential clients)
|
|
183
|
+
4. Set the **Callback URL** to `http://127.0.0.1:3391/callback`
|
|
184
|
+
5. Enable the required scopes (xc requests all of these automatically):
|
|
185
|
+
- `tweet.read`, `tweet.write` — read/write posts
|
|
186
|
+
- `tweet.moderate.write` — hide/unhide replies
|
|
187
|
+
- `users.read` — look up users
|
|
188
|
+
- `follows.read`, `follows.write` — manage follows
|
|
189
|
+
- `like.read`, `like.write` — manage likes
|
|
190
|
+
- `list.read`, `list.write` — manage lists
|
|
191
|
+
- `bookmark.read`, `bookmark.write` — manage bookmarks
|
|
192
|
+
- `block.read`, `block.write` — manage blocks
|
|
193
|
+
- `mute.read`, `mute.write` — manage mutes
|
|
194
|
+
- `dm.read`, `dm.write` — read/send DMs
|
|
195
|
+
- `media.write` — upload media
|
|
196
|
+
- `offline.access` — refresh tokens
|
|
197
|
+
|
|
198
|
+
### Login
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Interactive OAuth login (opens browser)
|
|
202
|
+
xc auth login --client-id <YOUR_CLIENT_ID>
|
|
203
|
+
|
|
204
|
+
# With client secret (for confidential apps — enables token refresh)
|
|
205
|
+
xc auth login --client-id <YOUR_CLIENT_ID> --client-secret <YOUR_SECRET>
|
|
206
|
+
|
|
207
|
+
# Check auth status
|
|
208
|
+
xc auth status
|
|
209
|
+
|
|
210
|
+
# Logout
|
|
211
|
+
xc auth logout
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Multiple Accounts
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Login with a named account
|
|
218
|
+
xc auth login --account work --client-id <CLIENT_ID>
|
|
219
|
+
|
|
220
|
+
# Switch default account
|
|
221
|
+
xc auth switch work
|
|
222
|
+
|
|
223
|
+
# Use a specific account for one command
|
|
224
|
+
xc search "query" --account work
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Credentials are stored in `~/.xc/config.json` (or `$XC_CONFIG_DIR/config.json`). Legacy `~/.config/xc/` configs are auto-migrated.
|
|
228
|
+
|
|
229
|
+
## Cost Tracking
|
|
230
|
+
|
|
231
|
+
Every API call is logged to `~/.xc/usage.jsonl` with timestamp, endpoint, method, and estimated cost. A cost footer is appended to every command's output.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
xc cost # Cost summary (1h, 24h, 7d, 30d)
|
|
235
|
+
xc cost --daily # Day-by-day breakdown
|
|
236
|
+
xc cost --json # Machine-readable summary
|
|
237
|
+
xc cost log # Raw request log (last 20)
|
|
238
|
+
xc cost log --limit 50 # More entries
|
|
239
|
+
xc cost log --json # Raw JSON log
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Suppress the per-command cost footer with `--quiet`:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
xc search "query" --quiet
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### API Usage Stats
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
xc usage # X API usage stats (tweet caps, etc.)
|
|
252
|
+
xc usage --json
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Budget Enforcement
|
|
256
|
+
|
|
257
|
+
Set daily spending limits to avoid surprise costs. Budget config lives in `~/.xc/budget.json`.
|
|
258
|
+
|
|
259
|
+
### Setting a Budget
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
xc budget set --daily 2.00 # Warn when over $2/day
|
|
263
|
+
xc budget set --daily 5.00 --action block # Block requests over $5/day
|
|
264
|
+
xc budget set --daily 1.00 --action confirm # Ask for confirmation when over
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Actions:**
|
|
268
|
+
- `warn` (default) — print a warning but allow the request
|
|
269
|
+
- `block` — reject the request with an error
|
|
270
|
+
- `confirm` — prompt interactively before proceeding
|
|
271
|
+
|
|
272
|
+
### Viewing Budget Status
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
xc budget show
|
|
276
|
+
# Budget:
|
|
277
|
+
#
|
|
278
|
+
# Daily limit: $2.00
|
|
279
|
+
# Today spent: $0.45 (22%)
|
|
280
|
+
# Remaining: $1.55
|
|
281
|
+
# Action: warn
|
|
282
|
+
# Locked: no
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Password Protection
|
|
286
|
+
|
|
287
|
+
Lock your budget so it can't be changed without a password:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
xc budget lock --password mysecret
|
|
291
|
+
|
|
292
|
+
# Now set/reset require --password
|
|
293
|
+
xc budget set --daily 10.00 --password mysecret
|
|
294
|
+
xc budget reset --password mysecret
|
|
295
|
+
|
|
296
|
+
# Remove the lock
|
|
297
|
+
xc budget unlock --password mysecret
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`show` and `cost` never require a password — only `set` and `reset` do.
|
|
301
|
+
|
|
302
|
+
### Removing Budget
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
xc budget reset # Remove budget config
|
|
306
|
+
xc budget reset --password pass # If locked
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Config
|
|
310
|
+
|
|
311
|
+
All configuration is stored in `~/.xc/` (or `$XC_CONFIG_DIR`):
|
|
312
|
+
|
|
313
|
+
| File | Contents |
|
|
314
|
+
|------|----------|
|
|
315
|
+
| `config.json` | Auth credentials (OAuth tokens, accounts) |
|
|
316
|
+
| `budget.json` | Budget limits and password lock |
|
|
317
|
+
| `usage.jsonl` | API request cost log (append-only) |
|
|
318
|
+
|
|
319
|
+
Legacy `~/.config/xc/` configs are auto-migrated on first run.
|
|
320
|
+
|
|
321
|
+
## Development
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
git clone https://github.com/cryppadotta/xc.git
|
|
325
|
+
cd xc
|
|
326
|
+
pnpm install
|
|
327
|
+
pnpm build
|
|
328
|
+
npm link # makes `xc` available globally
|
|
329
|
+
|
|
330
|
+
pnpm dev -- <command> # Run without building
|
|
331
|
+
pnpm build # Compile TypeScript
|
|
332
|
+
pnpm lint # Type check
|
|
333
|
+
pnpm test # Run tests (vitest)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Global Flags
|
|
337
|
+
|
|
338
|
+
| Flag | Description |
|
|
339
|
+
|------|-------------|
|
|
340
|
+
| `--quiet` | Suppress cost footer |
|
|
341
|
+
| `--json` | Raw JSON output (most commands) |
|
|
342
|
+
| `--account <name>` | Use a specific named account |
|
|
343
|
+
| `-V, --version` | Show version |
|
|
344
|
+
| `-h, --help` | Show help |
|
|
345
|
+
|
|
346
|
+
## License
|
|
347
|
+
|
|
348
|
+
MIT — forked from [jalehman/xc](https://github.com/jalehman/xc).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for bookmarks commands with mocked SDK client.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { registerBookmarksCommand, registerBookmarkCommand, registerUnbookmarkCommand, } from "../commands/bookmarks.js";
|
|
7
|
+
vi.mock("../lib/api.js", () => ({
|
|
8
|
+
getClient: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("../lib/resolve.js", () => ({
|
|
11
|
+
resolveAuthenticatedUserId: vi.fn(),
|
|
12
|
+
resolveUserId: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("../lib/cost.js", () => ({
|
|
15
|
+
logApiCall: vi.fn(),
|
|
16
|
+
formatCostFooter: vi.fn(() => ""),
|
|
17
|
+
estimateCost: vi.fn(() => 0),
|
|
18
|
+
loadUsageLog: vi.fn(() => []),
|
|
19
|
+
computeTodaySpend: vi.fn(() => 0),
|
|
20
|
+
}));
|
|
21
|
+
vi.mock("../lib/budget.js", () => ({
|
|
22
|
+
checkBudget: vi.fn(),
|
|
23
|
+
loadBudget: vi.fn(() => ({ action: "warn" })),
|
|
24
|
+
}));
|
|
25
|
+
import { getClient } from "../lib/api.js";
|
|
26
|
+
import { resolveAuthenticatedUserId } from "../lib/resolve.js";
|
|
27
|
+
describe("bookmarks command", () => {
|
|
28
|
+
let program;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
program = new Command();
|
|
31
|
+
program.exitOverride();
|
|
32
|
+
registerBookmarksCommand(program);
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
it("lists bookmarks", async () => {
|
|
36
|
+
vi.mocked(resolveAuthenticatedUserId).mockResolvedValue("myid");
|
|
37
|
+
const mockGetBookmarks = vi.fn().mockResolvedValue({
|
|
38
|
+
data: [
|
|
39
|
+
{
|
|
40
|
+
id: "t1",
|
|
41
|
+
text: "bookmarked post",
|
|
42
|
+
authorId: "u1",
|
|
43
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
includes: {
|
|
47
|
+
users: [{ id: "u1", username: "alice", name: "Alice" }],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
vi.mocked(getClient).mockResolvedValue({
|
|
51
|
+
users: { getBookmarks: mockGetBookmarks },
|
|
52
|
+
});
|
|
53
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
54
|
+
await program.parseAsync(["node", "xc", "bookmarks"]);
|
|
55
|
+
expect(mockGetBookmarks).toHaveBeenCalledWith("myid", expect.objectContaining({
|
|
56
|
+
maxResults: 20,
|
|
57
|
+
}));
|
|
58
|
+
expect(logSpy).toHaveBeenCalledWith("Bookmarks:\n");
|
|
59
|
+
logSpy.mockRestore();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("bookmark command", () => {
|
|
63
|
+
let program;
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
program = new Command();
|
|
66
|
+
program.exitOverride();
|
|
67
|
+
registerBookmarkCommand(program);
|
|
68
|
+
vi.clearAllMocks();
|
|
69
|
+
});
|
|
70
|
+
it("adds a bookmark", async () => {
|
|
71
|
+
vi.mocked(resolveAuthenticatedUserId).mockResolvedValue("myid");
|
|
72
|
+
const mockCreate = vi.fn().mockResolvedValue({ data: { bookmarked: true } });
|
|
73
|
+
vi.mocked(getClient).mockResolvedValue({
|
|
74
|
+
users: { createBookmark: mockCreate },
|
|
75
|
+
});
|
|
76
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
77
|
+
await program.parseAsync(["node", "xc", "bookmark", "12345"]);
|
|
78
|
+
expect(mockCreate).toHaveBeenCalledWith("myid", { tweetId: "12345" });
|
|
79
|
+
expect(logSpy).toHaveBeenCalledWith("Bookmarked post 12345");
|
|
80
|
+
logSpy.mockRestore();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("unbookmark command", () => {
|
|
84
|
+
let program;
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
program = new Command();
|
|
87
|
+
program.exitOverride();
|
|
88
|
+
registerUnbookmarkCommand(program);
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
});
|
|
91
|
+
it("removes a bookmark", async () => {
|
|
92
|
+
vi.mocked(resolveAuthenticatedUserId).mockResolvedValue("myid");
|
|
93
|
+
const mockDelete = vi.fn().mockResolvedValue({ data: { bookmarked: false } });
|
|
94
|
+
vi.mocked(getClient).mockResolvedValue({
|
|
95
|
+
users: { deleteBookmark: mockDelete },
|
|
96
|
+
});
|
|
97
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
98
|
+
await program.parseAsync(["node", "xc", "unbookmark", "12345"]);
|
|
99
|
+
expect(mockDelete).toHaveBeenCalledWith("myid", "12345");
|
|
100
|
+
expect(logSpy).toHaveBeenCalledWith("Unbookmarked post 12345");
|
|
101
|
+
logSpy.mockRestore();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=bookmarks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bookmarks.test.js","sourceRoot":"","sources":["../../src/__tests__/bookmarks.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for budget lib: password locking, hashing, verification.
|
|
3
|
+
* Uses vi.resetModules() + dynamic import so the config module's
|
|
4
|
+
* CONFIG_DIR constant picks up the temp dir from process.env.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
let origConfigDir;
|
|
11
|
+
let tmpDir;
|
|
12
|
+
let budget;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "xc-budget-test-"));
|
|
15
|
+
origConfigDir = process.env.XC_CONFIG_DIR;
|
|
16
|
+
process.env.XC_CONFIG_DIR = tmpDir;
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
budget = await import("../lib/budget.js");
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (origConfigDir !== undefined) {
|
|
22
|
+
process.env.XC_CONFIG_DIR = origConfigDir;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
delete process.env.XC_CONFIG_DIR;
|
|
26
|
+
}
|
|
27
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
describe("budget basics", () => {
|
|
30
|
+
it("returns defaults when no budget file exists", () => {
|
|
31
|
+
const b = budget.loadBudget();
|
|
32
|
+
expect(b.action).toBe("warn");
|
|
33
|
+
expect(b.daily).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it("saves and loads budget config", () => {
|
|
36
|
+
budget.saveBudget({ daily: 5.0, action: "block" });
|
|
37
|
+
const b = budget.loadBudget();
|
|
38
|
+
expect(b.daily).toBe(5.0);
|
|
39
|
+
expect(b.action).toBe("block");
|
|
40
|
+
});
|
|
41
|
+
it("resets budget by removing file", () => {
|
|
42
|
+
budget.saveBudget({ daily: 5.0, action: "warn" });
|
|
43
|
+
expect(fs.existsSync(budget.getBudgetPath())).toBe(true);
|
|
44
|
+
budget.resetBudget();
|
|
45
|
+
expect(fs.existsSync(budget.getBudgetPath())).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("password hashing", () => {
|
|
49
|
+
it("produces consistent hashes with same salt", () => {
|
|
50
|
+
const salt = "abcdef1234567890";
|
|
51
|
+
const h1 = budget.hashPassword("mypassword", salt);
|
|
52
|
+
const h2 = budget.hashPassword("mypassword", salt);
|
|
53
|
+
expect(h1).toBe(h2);
|
|
54
|
+
});
|
|
55
|
+
it("produces different hashes with different salts", () => {
|
|
56
|
+
const h1 = budget.hashPassword("mypassword", "salt1");
|
|
57
|
+
const h2 = budget.hashPassword("mypassword", "salt2");
|
|
58
|
+
expect(h1).not.toBe(h2);
|
|
59
|
+
});
|
|
60
|
+
it("produces different hashes for different passwords", () => {
|
|
61
|
+
const salt = "samesalt";
|
|
62
|
+
const h1 = budget.hashPassword("password1", salt);
|
|
63
|
+
const h2 = budget.hashPassword("password2", salt);
|
|
64
|
+
expect(h1).not.toBe(h2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("budget locking", () => {
|
|
68
|
+
it("reports unlocked when no password set", () => {
|
|
69
|
+
budget.saveBudget({ daily: 5.0, action: "warn" });
|
|
70
|
+
expect(budget.isLocked()).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it("locks budget with password", () => {
|
|
73
|
+
budget.saveBudget({ daily: 5.0, action: "warn" });
|
|
74
|
+
budget.lockBudget("secret123");
|
|
75
|
+
expect(budget.isLocked()).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it("verifies correct password", () => {
|
|
78
|
+
budget.saveBudget({ daily: 5.0, action: "warn" });
|
|
79
|
+
budget.lockBudget("secret123");
|
|
80
|
+
expect(budget.verifyPassword("secret123")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it("rejects incorrect password", () => {
|
|
83
|
+
budget.saveBudget({ daily: 5.0, action: "warn" });
|
|
84
|
+
budget.lockBudget("secret123");
|
|
85
|
+
expect(budget.verifyPassword("wrong")).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it("unlocks budget and removes password", () => {
|
|
88
|
+
budget.saveBudget({ daily: 5.0, action: "warn" });
|
|
89
|
+
budget.lockBudget("secret123");
|
|
90
|
+
expect(budget.isLocked()).toBe(true);
|
|
91
|
+
budget.unlockBudget();
|
|
92
|
+
expect(budget.isLocked()).toBe(false);
|
|
93
|
+
expect(budget.verifyPassword("anything")).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it("preserves budget config when locking", () => {
|
|
96
|
+
budget.saveBudget({ daily: 10.0, action: "block" });
|
|
97
|
+
budget.lockBudget("mypass");
|
|
98
|
+
const b = budget.loadBudget();
|
|
99
|
+
expect(b.daily).toBe(10.0);
|
|
100
|
+
expect(b.action).toBe("block");
|
|
101
|
+
expect(b.passwordHash).toBeDefined();
|
|
102
|
+
expect(b.passwordSalt).toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=budget.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.test.js","sourceRoot":"","sources":["../../src/__tests__/budget.test.ts"],"names":[],"mappings":"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"}
|