chirpie 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.
Files changed (2) hide show
  1. package/dist/index.js +417 -0
  2. package/package.json +27 -0
package/dist/index.js ADDED
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command9 } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/lib/config.ts
10
+ import { getConfig, saveConfig, deleteConfig, requireConfig } from "@chirpie/sdk";
11
+
12
+ // src/lib/output.ts
13
+ function json(data) {
14
+ console.log(JSON.stringify(data, null, 2));
15
+ }
16
+ function table(rows) {
17
+ if (rows.length === 0) {
18
+ console.log("No results.");
19
+ return;
20
+ }
21
+ console.table(rows);
22
+ }
23
+ function success(msg) {
24
+ console.log(`\x1B[32m\u2713\x1B[0m ${msg}`);
25
+ }
26
+ function error(msg) {
27
+ console.error(`\x1B[31m\u2717\x1B[0m ${msg}`);
28
+ }
29
+ function info(msg) {
30
+ console.log(`\x1B[36m\u2139\x1B[0m ${msg}`);
31
+ }
32
+
33
+ // src/commands/auth.ts
34
+ import { createInterface } from "readline";
35
+ var authCommand = new Command("auth").description("Configure your Chirpie API key").option("--key <key>", "API key (or enter interactively)").option("--base-url <url>", "Base URL (default: https://chirpie.ai)").action(async (opts) => {
36
+ let apiKey = opts.key;
37
+ if (!apiKey) {
38
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
39
+ apiKey = await new Promise((resolve) => {
40
+ rl.question("Enter your Chirpie API key: ", (answer) => {
41
+ rl.close();
42
+ resolve(answer.trim());
43
+ });
44
+ });
45
+ }
46
+ if (!apiKey || !apiKey.startsWith("chirpie_sk_")) {
47
+ console.error("Invalid API key. Keys start with chirpie_sk_");
48
+ process.exit(1);
49
+ }
50
+ saveConfig(apiKey, opts.baseUrl);
51
+ success("API key saved to ~/.chirpie/config.json");
52
+ });
53
+ var whoamiCommand = new Command("whoami").description("Check your authentication status").action(() => {
54
+ const config = getConfig();
55
+ if (!config) {
56
+ console.log("Not authenticated. Run: chirpie auth");
57
+ return;
58
+ }
59
+ info(`API key: ${config.api_key.slice(0, 20)}...`);
60
+ info(`Base URL: ${config.base_url}`);
61
+ });
62
+
63
+ // src/commands/login.ts
64
+ import { Command as Command2 } from "commander";
65
+ import { createServer } from "http";
66
+ import { randomBytes } from "crypto";
67
+ import { exec } from "child_process";
68
+ import { platform } from "os";
69
+ function openBrowser(url) {
70
+ const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
71
+ exec(`${cmd} "${url}"`);
72
+ }
73
+ var loginCommand = new Command2("login").description("Authenticate with your Chirpie account (opens browser)").option("--base-url <url>", "Base URL", "https://chirpie.ai").action(async (opts) => {
74
+ const existing = getConfig();
75
+ if (existing) {
76
+ info(`Already authenticated (${existing.api_key.slice(0, 20)}...)`);
77
+ info("Run chirpie logout to sign out first, or chirpie auth --key to change keys.");
78
+ return;
79
+ }
80
+ const state = randomBytes(16).toString("hex");
81
+ const baseUrl = opts.baseUrl;
82
+ const server = createServer((req, res) => {
83
+ const url = new URL(req.url ?? "/", `http://localhost`);
84
+ if (url.pathname === "/callback") {
85
+ const key = url.searchParams.get("key");
86
+ const returnedState = url.searchParams.get("state");
87
+ res.writeHead(200, {
88
+ "Content-Type": "text/html",
89
+ "Access-Control-Allow-Origin": "*"
90
+ });
91
+ if (!key || returnedState !== state) {
92
+ res.end("<html><body><h2>Authorization failed.</h2><p>State mismatch or missing key. Please try again.</p></body></html>");
93
+ error("Authorization failed \u2014 state mismatch");
94
+ server.close();
95
+ process.exit(1);
96
+ }
97
+ res.end("<html><body><h2>CLI authorized!</h2><p>You can close this tab.</p></body></html>");
98
+ saveConfig(key, baseUrl);
99
+ console.log("");
100
+ success("Logged in to Chirpie!");
101
+ info(`API key saved to ~/.chirpie/config.json`);
102
+ server.close();
103
+ process.exit(0);
104
+ } else {
105
+ res.writeHead(200, {
106
+ "Access-Control-Allow-Origin": "*",
107
+ "Access-Control-Allow-Methods": "GET"
108
+ });
109
+ res.end();
110
+ }
111
+ });
112
+ server.listen(0, () => {
113
+ const addr = server.address();
114
+ if (!addr || typeof addr === "string") {
115
+ error("Failed to start local server");
116
+ process.exit(1);
117
+ }
118
+ const port = addr.port;
119
+ const authUrl = `${baseUrl}/auth/cli?port=${port}&state=${state}`;
120
+ console.log("");
121
+ info("Opening browser to authenticate...");
122
+ info(`If the browser doesn't open, visit:
123
+
124
+ ${authUrl}
125
+ `);
126
+ openBrowser(authUrl);
127
+ setTimeout(() => {
128
+ error("Login timed out. Please try again.");
129
+ server.close();
130
+ process.exit(1);
131
+ }, 5 * 60 * 1e3);
132
+ });
133
+ });
134
+ var logoutCommand = new Command2("logout").description("Remove stored authentication").action(async () => {
135
+ const { deleteConfig: deleteConfig2 } = await import("@chirpie/sdk");
136
+ if (deleteConfig2()) {
137
+ success("Logged out. API key removed from ~/.chirpie/config.json");
138
+ } else {
139
+ info("Not logged in.");
140
+ }
141
+ });
142
+
143
+ // src/commands/post.ts
144
+ import { Command as Command3 } from "commander";
145
+
146
+ // src/lib/client.ts
147
+ import { ChirpieClient } from "@chirpie/sdk";
148
+ function getClient() {
149
+ const config = requireConfig();
150
+ return new ChirpieClient({
151
+ apiKey: config.api_key,
152
+ baseUrl: config.base_url
153
+ });
154
+ }
155
+
156
+ // src/commands/post.ts
157
+ var postCommand = new Command3("post").description("Create a post on X").argument("<text>", "Post text (max 280 chars)").option("-a, --account <id>", "Account ID").option("-s, --schedule <datetime>", "Schedule for later (ISO 8601)").option("--json", "Output raw JSON").action(async (text, opts) => {
158
+ const client = getClient();
159
+ let accountId = opts.account;
160
+ if (!accountId) {
161
+ const accounts = await client.listAccounts();
162
+ const active = accounts.filter((a) => a.is_active);
163
+ if (active.length === 0) {
164
+ error("No connected accounts. Run: chirpie accounts connect");
165
+ process.exit(1);
166
+ }
167
+ if (active.length > 1) {
168
+ error("Multiple accounts found. Use --account <id> to specify:");
169
+ active.forEach((a) => console.log(` ${a.id} @${a.x_username}`));
170
+ process.exit(1);
171
+ }
172
+ accountId = active[0].id;
173
+ }
174
+ try {
175
+ const post = await client.createPost({
176
+ account_id: accountId,
177
+ text,
178
+ schedule_at: opts.schedule
179
+ });
180
+ if (opts.json) {
181
+ json(post);
182
+ } else if (post.status === "scheduled") {
183
+ success(`Post scheduled for ${post.scheduled_at} (${post.id})`);
184
+ } else {
185
+ success(`Post created (${post.id})`);
186
+ }
187
+ } catch (err) {
188
+ error(err instanceof Error ? err.message : "Failed to create post");
189
+ process.exit(1);
190
+ }
191
+ });
192
+
193
+ // src/commands/thread.ts
194
+ import { Command as Command4 } from "commander";
195
+ var threadCommand = new Command4("thread").description("Create a thread on X").argument("<posts...>", "Thread posts (each argument is one post)").option("-a, --account <id>", "Account ID").option("-s, --schedule <datetime>", "Schedule for later (ISO 8601)").option("--json", "Output raw JSON").action(async (posts, opts) => {
196
+ const client = getClient();
197
+ let accountId = opts.account;
198
+ if (!accountId) {
199
+ const accounts = await client.listAccounts();
200
+ const active = accounts.filter((a) => a.is_active);
201
+ if (active.length === 0) {
202
+ error("No connected accounts. Run: chirpie accounts connect");
203
+ process.exit(1);
204
+ }
205
+ if (active.length > 1) {
206
+ error("Multiple accounts found. Use --account <id> to specify:");
207
+ active.forEach((a) => console.log(` ${a.id} @${a.x_username}`));
208
+ process.exit(1);
209
+ }
210
+ accountId = active[0].id;
211
+ }
212
+ if (posts.length < 2) {
213
+ error("Thread must have at least 2 posts");
214
+ process.exit(1);
215
+ }
216
+ try {
217
+ const thread = await client.createThread({
218
+ account_id: accountId,
219
+ posts: posts.map((text) => ({ text })),
220
+ schedule_at: opts.schedule
221
+ });
222
+ if (opts.json) {
223
+ json(thread);
224
+ } else {
225
+ success(`Thread created with ${thread.posts.length} posts (${thread.id})`);
226
+ }
227
+ } catch (err) {
228
+ error(err instanceof Error ? err.message : "Failed to create thread");
229
+ process.exit(1);
230
+ }
231
+ });
232
+
233
+ // src/commands/posts.ts
234
+ import { Command as Command5 } from "commander";
235
+ var postsCommand = new Command5("posts").description("List, get, or delete posts").option("--status <status>", "Filter by status").option("--account <id>", "Filter by account").option("--limit <n>", "Limit results", "20").option("--json", "Output raw JSON").action(async (opts) => {
236
+ const client = getClient();
237
+ try {
238
+ const posts = await client.listPosts({
239
+ status: opts.status,
240
+ account_id: opts.account,
241
+ limit: parseInt(opts.limit)
242
+ });
243
+ if (opts.json) {
244
+ json(posts);
245
+ } else {
246
+ table(
247
+ posts.map((p) => ({
248
+ id: p.id.slice(0, 8),
249
+ status: p.status,
250
+ text: p.text.length > 50 ? p.text.slice(0, 50) + "..." : p.text,
251
+ created: p.created_at.slice(0, 10)
252
+ }))
253
+ );
254
+ }
255
+ } catch (err) {
256
+ error(err instanceof Error ? err.message : "Failed to list posts");
257
+ process.exit(1);
258
+ }
259
+ });
260
+ postsCommand.command("get <id>").description("Get a post by ID").option("--json", "Output raw JSON").action(async (id, opts) => {
261
+ const client = getClient();
262
+ try {
263
+ const post = await client.getPost(id);
264
+ if (opts.json) {
265
+ json(post);
266
+ } else {
267
+ console.log(`ID: ${post.id}`);
268
+ console.log(`Status: ${post.status}`);
269
+ console.log(`Text: ${post.text}`);
270
+ console.log(`Created: ${post.created_at}`);
271
+ if (post.scheduled_at) console.log(`Scheduled: ${post.scheduled_at}`);
272
+ if (post.published_at) console.log(`Published: ${post.published_at}`);
273
+ if (post.platform_post_id) console.log(`X Post ID: ${post.platform_post_id}`);
274
+ }
275
+ } catch (err) {
276
+ error(err instanceof Error ? err.message : "Failed to get post");
277
+ process.exit(1);
278
+ }
279
+ });
280
+ postsCommand.command("delete <id>").description("Delete a post").action(async (id) => {
281
+ const client = getClient();
282
+ try {
283
+ await client.deletePost(id);
284
+ success(`Post ${id} deleted`);
285
+ } catch (err) {
286
+ error(err instanceof Error ? err.message : "Failed to delete post");
287
+ process.exit(1);
288
+ }
289
+ });
290
+
291
+ // src/commands/accounts.ts
292
+ import { Command as Command6 } from "commander";
293
+ var accountsCommand = new Command6("accounts").description("List connected X accounts").option("--json", "Output raw JSON").action(async (opts) => {
294
+ const client = getClient();
295
+ try {
296
+ const accounts = await client.listAccounts();
297
+ if (opts.json) {
298
+ json(accounts);
299
+ } else {
300
+ table(
301
+ accounts.map((a) => ({
302
+ id: a.id.slice(0, 8),
303
+ username: `@${a.x_username}`,
304
+ name: a.x_display_name ?? "",
305
+ active: a.is_active ? "\u2713" : "\u2717"
306
+ }))
307
+ );
308
+ }
309
+ } catch (err) {
310
+ error(err instanceof Error ? err.message : "Failed to list accounts");
311
+ process.exit(1);
312
+ }
313
+ });
314
+ accountsCommand.command("connect").description("Connect a new X account").action(async () => {
315
+ const client = getClient();
316
+ try {
317
+ const result = await client.connectAccount();
318
+ info("Open this URL in your browser to connect your X account:");
319
+ console.log(`
320
+ ${result.authorization_url}
321
+ `);
322
+ } catch (err) {
323
+ error(err instanceof Error ? err.message : "Failed to start connection");
324
+ process.exit(1);
325
+ }
326
+ });
327
+
328
+ // src/commands/analytics.ts
329
+ import { Command as Command7 } from "commander";
330
+ var analyticsCommand = new Command7("analytics").description("Get analytics for a post").argument("<post_id>", "Post ID").option("--json", "Output raw JSON").action(async (postId, opts) => {
331
+ const client = getClient();
332
+ try {
333
+ const analytics = await client.getPostAnalytics(postId);
334
+ if (opts.json) {
335
+ json(analytics);
336
+ } else {
337
+ console.log(`Post: ${analytics.post_id}`);
338
+ console.log(`Impressions: ${analytics.impressions.toLocaleString()}`);
339
+ console.log(`Likes: ${analytics.likes.toLocaleString()}`);
340
+ console.log(`Retweets: ${analytics.retweets.toLocaleString()}`);
341
+ console.log(`Replies: ${analytics.replies.toLocaleString()}`);
342
+ console.log(`Quotes: ${analytics.quotes.toLocaleString()}`);
343
+ console.log(`Bookmarks: ${analytics.bookmarks.toLocaleString()}`);
344
+ console.log(`Clicks: ${analytics.clicks.toLocaleString()}`);
345
+ console.log(`Fetched: ${analytics.fetched_at}`);
346
+ }
347
+ } catch (err) {
348
+ error(err instanceof Error ? err.message : "Failed to get analytics");
349
+ process.exit(1);
350
+ }
351
+ });
352
+
353
+ // src/commands/keys.ts
354
+ import { Command as Command8 } from "commander";
355
+ var keysCommand = new Command8("keys").description("Manage API keys").option("--json", "Output raw JSON").action(async (opts) => {
356
+ const client = getClient();
357
+ try {
358
+ const keys = await client.listKeys();
359
+ if (opts.json) {
360
+ json(keys);
361
+ } else {
362
+ table(
363
+ keys.map((k) => ({
364
+ id: k.id.slice(0, 8),
365
+ name: k.name,
366
+ prefix: k.key_prefix,
367
+ revoked: k.revoked ? "\u2717" : "",
368
+ last_used: k.last_used_at?.slice(0, 10) ?? "never"
369
+ }))
370
+ );
371
+ }
372
+ } catch (err) {
373
+ error(err instanceof Error ? err.message : "Failed to list keys");
374
+ process.exit(1);
375
+ }
376
+ });
377
+ keysCommand.command("create").description("Create a new API key").option("-n, --name <name>", "Key name").option("--json", "Output raw JSON").action(async (opts) => {
378
+ const client = getClient();
379
+ try {
380
+ const key = await client.createKey(opts.name);
381
+ if (opts.json) {
382
+ json(key);
383
+ } else {
384
+ success("API key created. Save it \u2014 you won't see it again.");
385
+ console.log(`
386
+ ${key.key}
387
+ `);
388
+ }
389
+ } catch (err) {
390
+ error(err instanceof Error ? err.message : "Failed to create key");
391
+ process.exit(1);
392
+ }
393
+ });
394
+ keysCommand.command("revoke <id>").description("Revoke an API key").action(async (id) => {
395
+ const client = getClient();
396
+ try {
397
+ await client.revokeKey(id);
398
+ success(`Key ${id} revoked`);
399
+ } catch (err) {
400
+ error(err instanceof Error ? err.message : "Failed to revoke key");
401
+ process.exit(1);
402
+ }
403
+ });
404
+
405
+ // src/index.ts
406
+ var program = new Command9().name("chirpie").description("Chirpie \u2014 the social media layer for AI agents").version("0.1.0");
407
+ program.addCommand(loginCommand);
408
+ program.addCommand(logoutCommand);
409
+ program.addCommand(authCommand);
410
+ program.addCommand(whoamiCommand);
411
+ program.addCommand(postCommand);
412
+ program.addCommand(threadCommand);
413
+ program.addCommand(postsCommand);
414
+ program.addCommand(accountsCommand);
415
+ program.addCommand(analyticsCommand);
416
+ program.addCommand(keysCommand);
417
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "chirpie",
3
+ "version": "0.1.0",
4
+ "description": "Chirpie CLI — post to social media from the command line",
5
+ "repository": { "type": "git", "url": "https://github.com/Firefloco/chirpie", "directory": "packages/cli" },
6
+ "homepage": "https://chirpie.ai/docs/cli",
7
+ "publishConfig": { "access": "public" },
8
+ "type": "module",
9
+ "bin": {
10
+ "chirpie": "./dist/index.js"
11
+ },
12
+ "files": ["dist"],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch"
16
+ },
17
+ "keywords": ["chirpie", "cli", "social-media", "twitter", "x"],
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "@chirpie/sdk": "^0.1.0",
21
+ "commander": "^13"
22
+ },
23
+ "devDependencies": {
24
+ "tsup": "^8",
25
+ "typescript": "^5"
26
+ }
27
+ }