pythx-cli 0.0.2 → 0.0.4

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/bin/pythx.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js");
package/dist/cli.js ADDED
@@ -0,0 +1,658 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+ import meow from "meow";
6
+
7
+ // src/app.tsx
8
+ import { useState as useState3 } from "react";
9
+ import { Box as Box5, Text as Text5, useApp, useInput } from "ink";
10
+ import chalk5 from "chalk";
11
+
12
+ // src/components/header.tsx
13
+ import { useState, useEffect } from "react";
14
+ import { Box, Text } from "ink";
15
+ import chalk from "chalk";
16
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
+ function Header({ lastUpdated, loading }) {
18
+ const [secondsAgo, setSecondsAgo] = useState(0);
19
+ useEffect(() => {
20
+ if (!lastUpdated) return;
21
+ setSecondsAgo(Math.round((Date.now() - lastUpdated.getTime()) / 1e3));
22
+ const interval = setInterval(() => {
23
+ setSecondsAgo(Math.round((Date.now() - lastUpdated.getTime()) / 1e3));
24
+ }, 1e3);
25
+ return () => clearInterval(interval);
26
+ }, [lastUpdated]);
27
+ return /* @__PURE__ */ jsxs(
28
+ Box,
29
+ {
30
+ borderStyle: "single",
31
+ borderColor: "green",
32
+ paddingX: 1,
33
+ justifyContent: "space-between",
34
+ children: [
35
+ /* @__PURE__ */ jsxs(Text, { children: [
36
+ chalk.green.bold("PYTHX"),
37
+ chalk.dim(" \u2591\u2591 "),
38
+ chalk.white("SENTIMENT TERMINAL")
39
+ ] }),
40
+ /* @__PURE__ */ jsx(Text, { children: loading ? chalk.yellow("\u27F3 Loading...") : lastUpdated ? /* @__PURE__ */ jsxs(Fragment, { children: [
41
+ chalk.green("\u25CF "),
42
+ chalk.dim(`LIVE \xB7 ${secondsAgo}s ago`)
43
+ ] }) : chalk.dim("Waiting...") })
44
+ ]
45
+ }
46
+ );
47
+ }
48
+
49
+ // src/components/entity-row.tsx
50
+ import { Box as Box2, Text as Text2 } from "ink";
51
+ import chalk2 from "chalk";
52
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
53
+ var COL = {
54
+ name: 15,
55
+ score: 8,
56
+ trend: 10,
57
+ stat: 5
58
+ };
59
+ function formatScore(score) {
60
+ const prefix = score >= 0 ? "+" : "";
61
+ return `${prefix}${score.toFixed(2)}`;
62
+ }
63
+ function sparkline(distribution) {
64
+ const { positive, neutral, negative, total } = distribution;
65
+ if (total === 0) return chalk2.dim("\u2591".repeat(COL.trend));
66
+ const posWidth = Math.round(positive / total * COL.trend);
67
+ const negWidth = Math.round(negative / total * COL.trend);
68
+ const neuWidth = COL.trend - posWidth - negWidth;
69
+ return chalk2.green("\u2588".repeat(posWidth)) + chalk2.gray("\u2591".repeat(Math.max(0, neuWidth))) + chalk2.red("\u2588".repeat(negWidth));
70
+ }
71
+ function EntityRow({ analysis, isActive }) {
72
+ const { entity, snapshot } = analysis;
73
+ const { distribution } = snapshot;
74
+ const cursor = isActive ? chalk2.green("\u25B6 ") : " ";
75
+ const name = entity.name.length > COL.name ? entity.name.substring(0, COL.name - 1) + "\u2026" : entity.name.padEnd(COL.name);
76
+ const score = formatScore(snapshot.averageScore).padStart(COL.score);
77
+ const trend = sparkline(distribution);
78
+ const pos = String(distribution.positive).padStart(COL.stat);
79
+ const neu = String(distribution.neutral).padStart(COL.stat);
80
+ const neg = String(distribution.negative).padStart(COL.stat);
81
+ const tot = String(distribution.total).padStart(COL.stat);
82
+ const colorName = isActive ? chalk2.green.bold(name) : chalk2.white(name);
83
+ const colorScore = snapshot.averageScore > 0.05 ? chalk2.green(score) : snapshot.averageScore < -0.05 ? chalk2.red(score) : chalk2.gray(score);
84
+ return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { children: [
85
+ cursor,
86
+ colorName,
87
+ colorScore,
88
+ " ",
89
+ trend,
90
+ chalk2.green(pos),
91
+ chalk2.gray(neu),
92
+ chalk2.red(neg),
93
+ chalk2.dim(tot)
94
+ ] }) });
95
+ }
96
+
97
+ // src/components/detail-panel.tsx
98
+ import { Box as Box3, Text as Text3 } from "ink";
99
+ import chalk3 from "chalk";
100
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
101
+ function formatPost(post) {
102
+ const { sentiment } = post;
103
+ const labelMap = {
104
+ positive: chalk3.green,
105
+ negative: chalk3.red,
106
+ neutral: chalk3.gray
107
+ };
108
+ const colorFn = labelMap[sentiment.label];
109
+ const tag = colorFn(
110
+ `[${sentiment.label.substring(0, 3).toUpperCase()} ${(sentiment.score * 100).toFixed(0)}%]`
111
+ );
112
+ const author = post.authorUsername ? chalk3.dim(`@${post.authorUsername}`) : chalk3.dim("@unknown");
113
+ const text2 = post.text.replace(/\n/g, " ").substring(0, 70).trim();
114
+ const ellipsis = post.text.length > 70 ? chalk3.dim("...") : "";
115
+ return ` ${tag} ${author}: "${text2}${ellipsis}"`;
116
+ }
117
+ function DetailPanel({ analysis }) {
118
+ const { entity, snapshot } = analysis;
119
+ const scoreColor = snapshot.averageScore > 0.05 ? chalk3.green : snapshot.averageScore < -0.05 ? chalk3.red : chalk3.gray;
120
+ const allPosts = [
121
+ ...snapshot.topPositive,
122
+ ...snapshot.topNegative
123
+ ].sort((a, b) => b.sentiment.score - a.sentiment.score);
124
+ return /* @__PURE__ */ jsxs3(
125
+ Box3,
126
+ {
127
+ flexDirection: "column",
128
+ borderStyle: "single",
129
+ borderColor: "gray",
130
+ paddingX: 1,
131
+ children: [
132
+ /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
133
+ /* @__PURE__ */ jsxs3(Text3, { children: [
134
+ chalk3.green("\u25B6"),
135
+ " ",
136
+ chalk3.bold(entity.name.toUpperCase()),
137
+ chalk3.dim(" \u2014 Detail")
138
+ ] }),
139
+ /* @__PURE__ */ jsxs3(Text3, { children: [
140
+ chalk3.dim("avg: "),
141
+ scoreColor(
142
+ `${snapshot.averageScore >= 0 ? "+" : ""}${snapshot.averageScore.toFixed(3)}`
143
+ )
144
+ ] })
145
+ ] }),
146
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: allPosts.length === 0 ? /* @__PURE__ */ jsx3(Text3, { children: chalk3.dim(" No posts to display") }) : allPosts.slice(0, 8).map((post) => /* @__PURE__ */ jsx3(Text3, { children: formatPost(post) }, post.id)) })
147
+ ]
148
+ }
149
+ );
150
+ }
151
+
152
+ // src/components/status-bar.tsx
153
+ import { Box as Box4, Text as Text4 } from "ink";
154
+ import chalk4 from "chalk";
155
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
156
+ function StatusBar() {
157
+ return /* @__PURE__ */ jsx4(
158
+ Box4,
159
+ {
160
+ borderStyle: "single",
161
+ borderColor: "gray",
162
+ paddingX: 1,
163
+ justifyContent: "center",
164
+ children: /* @__PURE__ */ jsxs4(Text4, { children: [
165
+ chalk4.dim("\u2591 "),
166
+ chalk4.white("q"),
167
+ chalk4.dim(":quit "),
168
+ chalk4.white("\u2191\u2193"),
169
+ chalk4.dim(":navigate "),
170
+ chalk4.white("enter"),
171
+ chalk4.dim(":expand "),
172
+ chalk4.white("r"),
173
+ chalk4.dim(":refresh")
174
+ ] })
175
+ }
176
+ );
177
+ }
178
+
179
+ // src/hooks/use-live-data.ts
180
+ import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
181
+
182
+ // ../core/src/entities.ts
183
+ var DEFAULT_ENTITIES = [
184
+ {
185
+ id: "trump",
186
+ name: "Trump",
187
+ type: "person",
188
+ queries: [{ source: "x", query: "Trump -is:retweet lang:en" }],
189
+ icon: "user"
190
+ },
191
+ {
192
+ id: "bitcoin",
193
+ name: "Bitcoin",
194
+ type: "asset",
195
+ queries: [
196
+ {
197
+ source: "x",
198
+ query: "(Bitcoin OR BTC OR #Bitcoin) -is:retweet lang:en"
199
+ }
200
+ ],
201
+ icon: "bitcoin"
202
+ },
203
+ {
204
+ id: "ai",
205
+ name: "Artificial Intelligence",
206
+ type: "topic",
207
+ queries: [
208
+ {
209
+ source: "x",
210
+ query: '("artificial intelligence" OR #AI OR "machine learning") -is:retweet lang:en'
211
+ }
212
+ ],
213
+ icon: "brain"
214
+ }
215
+ ];
216
+
217
+ // ../core/src/providers/x.ts
218
+ var X_API_BASE = "https://api.x.com/2";
219
+ var HTML_ENTITIES = {
220
+ "&amp;": "&",
221
+ "&lt;": "<",
222
+ "&gt;": ">",
223
+ "&quot;": '"',
224
+ "&#39;": "'",
225
+ "&apos;": "'"
226
+ };
227
+ function decodeHtmlEntities(text2) {
228
+ return text2.replace(
229
+ /&(?:amp|lt|gt|quot|#39|apos);/g,
230
+ (match) => HTML_ENTITIES[match] ?? match
231
+ );
232
+ }
233
+ var XProvider = class {
234
+ type = "x";
235
+ async fetchPosts(query, options) {
236
+ const token = process.env.X_BEARER_TOKEN;
237
+ if (!token) {
238
+ throw new Error("X_BEARER_TOKEN environment variable is required");
239
+ }
240
+ const params = new URLSearchParams({
241
+ query,
242
+ max_results: String(Math.min(Math.max(options.count, 10), 100)),
243
+ "tweet.fields": "created_at,public_metrics,author_id",
244
+ expansions: "author_id",
245
+ "user.fields": "username"
246
+ });
247
+ if (options.sinceId) {
248
+ params.set("since_id", options.sinceId);
249
+ }
250
+ const url = `${X_API_BASE}/tweets/search/recent?${params}`;
251
+ const response = await fetch(url, {
252
+ headers: {
253
+ Authorization: `Bearer ${token}`
254
+ }
255
+ });
256
+ if (!response.ok) {
257
+ const errorText = await response.text();
258
+ throw new Error(
259
+ `X API error (${response.status}): ${errorText}`
260
+ );
261
+ }
262
+ const json = await response.json();
263
+ if (!json.data || json.data.length === 0) {
264
+ return [];
265
+ }
266
+ const userMap = /* @__PURE__ */ new Map();
267
+ if (json.includes?.users) {
268
+ for (const user of json.includes.users) {
269
+ userMap.set(user.id, user.username);
270
+ }
271
+ }
272
+ return json.data.map((tweet) => ({
273
+ id: tweet.id,
274
+ source: "x",
275
+ text: decodeHtmlEntities(tweet.text),
276
+ authorId: tweet.author_id,
277
+ authorUsername: userMap.get(tweet.author_id),
278
+ createdAt: tweet.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
279
+ url: `https://x.com/i/status/${tweet.id}`,
280
+ metrics: {
281
+ upvotes: tweet.public_metrics?.like_count ?? 0,
282
+ comments: tweet.public_metrics?.reply_count ?? 0,
283
+ shares: tweet.public_metrics?.retweet_count ?? 0
284
+ }
285
+ }));
286
+ }
287
+ getModelId() {
288
+ return "cardiffnlp/twitter-roberta-base-sentiment-latest";
289
+ }
290
+ };
291
+
292
+ // ../core/src/providers/index.ts
293
+ function getProvider(source) {
294
+ switch (source) {
295
+ case "x":
296
+ return new XProvider();
297
+ default:
298
+ throw new Error(`Provider not implemented: ${source}`);
299
+ }
300
+ }
301
+
302
+ // ../core/src/sentiment/classify.ts
303
+ import { HfInference } from "@huggingface/inference";
304
+
305
+ // ../core/src/sentiment/models.ts
306
+ var MODEL_LABEL_MAP = {
307
+ "cardiffnlp/twitter-roberta-base-sentiment-latest": {
308
+ positive: "positive",
309
+ negative: "negative",
310
+ neutral: "neutral"
311
+ },
312
+ "nlptown/bert-base-multilingual-uncased-sentiment": {
313
+ "1 star": "negative",
314
+ "2 stars": "negative",
315
+ "3 stars": "neutral",
316
+ "4 stars": "positive",
317
+ "5 stars": "positive"
318
+ },
319
+ "ProsusAI/finbert": {
320
+ positive: "positive",
321
+ negative: "negative",
322
+ neutral: "neutral"
323
+ },
324
+ "distilbert-base-uncased-finetuned-sst-2-english": {
325
+ POSITIVE: "positive",
326
+ NEGATIVE: "negative"
327
+ }
328
+ };
329
+ function normalizeLabel(modelId, rawLabel) {
330
+ const map = MODEL_LABEL_MAP[modelId];
331
+ if (map) {
332
+ const normalized = map[rawLabel];
333
+ if (normalized) return normalized;
334
+ }
335
+ const lower = rawLabel.toLowerCase();
336
+ if (lower.includes("positive")) return "positive";
337
+ if (lower.includes("negative")) return "negative";
338
+ return "neutral";
339
+ }
340
+
341
+ // ../core/src/sentiment/classify.ts
342
+ var hf = null;
343
+ function getClient() {
344
+ if (!hf) {
345
+ const token = process.env.HF_API_TOKEN;
346
+ if (!token) {
347
+ throw new Error("HF_API_TOKEN environment variable is required");
348
+ }
349
+ hf = new HfInference(token);
350
+ }
351
+ return hf;
352
+ }
353
+ async function classifyPost(text2, modelId) {
354
+ const client = getClient();
355
+ const results = await client.textClassification({
356
+ model: modelId,
357
+ inputs: text2
358
+ });
359
+ if (!results || results.length === 0) {
360
+ return { label: "neutral", score: 0.5 };
361
+ }
362
+ const top = results[0];
363
+ return {
364
+ label: normalizeLabel(modelId, top.label),
365
+ score: top.score
366
+ };
367
+ }
368
+ var BATCH_SIZE = 5;
369
+ async function classifyPosts(posts2, modelId) {
370
+ const results = [];
371
+ for (let i = 0; i < posts2.length; i += BATCH_SIZE) {
372
+ const batch = posts2.slice(i, i + BATCH_SIZE);
373
+ const classified = await Promise.all(
374
+ batch.map(async (post) => {
375
+ const sentiment = await classifyPost(post.text, modelId);
376
+ return { ...post, sentiment };
377
+ })
378
+ );
379
+ results.push(...classified);
380
+ }
381
+ return results;
382
+ }
383
+
384
+ // ../core/src/aggregator.ts
385
+ function computeDistribution(posts2) {
386
+ let positive = 0;
387
+ let negative = 0;
388
+ let neutral = 0;
389
+ for (const post of posts2) {
390
+ switch (post.sentiment.label) {
391
+ case "positive":
392
+ positive++;
393
+ break;
394
+ case "negative":
395
+ negative++;
396
+ break;
397
+ case "neutral":
398
+ neutral++;
399
+ break;
400
+ }
401
+ }
402
+ return { positive, negative, neutral, total: posts2.length };
403
+ }
404
+ function computeAverageScore(posts2) {
405
+ if (posts2.length === 0) return 0;
406
+ const sum = posts2.reduce((acc, post) => {
407
+ if (post.sentiment.label === "positive") return acc + post.sentiment.score;
408
+ if (post.sentiment.label === "negative") return acc - post.sentiment.score;
409
+ return acc;
410
+ }, 0);
411
+ return sum / posts2.length;
412
+ }
413
+ function aggregate(posts2) {
414
+ const distribution = computeDistribution(posts2);
415
+ const averageScore = computeAverageScore(posts2);
416
+ const topPositive = posts2.filter((p) => p.sentiment.label === "positive").sort((a, b) => b.sentiment.score - a.sentiment.score).slice(0, 5);
417
+ const topNegative = posts2.filter((p) => p.sentiment.label === "negative").sort((a, b) => b.sentiment.score - a.sentiment.score).slice(0, 5);
418
+ const sourceGroups = /* @__PURE__ */ new Map();
419
+ for (const post of posts2) {
420
+ const group = sourceGroups.get(post.source) ?? [];
421
+ group.push(post);
422
+ sourceGroups.set(post.source, group);
423
+ }
424
+ const bySource = [];
425
+ for (const [source, sourcePosts] of sourceGroups) {
426
+ bySource.push({
427
+ source,
428
+ distribution: computeDistribution(sourcePosts),
429
+ averageScore: computeAverageScore(sourcePosts)
430
+ });
431
+ }
432
+ return {
433
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
434
+ distribution,
435
+ bySource,
436
+ averageScore,
437
+ topPositive,
438
+ topNegative
439
+ };
440
+ }
441
+
442
+ // ../core/src/db/client.ts
443
+ import { drizzle } from "drizzle-orm/postgres-js";
444
+ import postgres from "postgres";
445
+
446
+ // ../core/src/db/schema.ts
447
+ import {
448
+ pgTable,
449
+ serial,
450
+ varchar,
451
+ text,
452
+ integer,
453
+ real,
454
+ timestamp,
455
+ jsonb,
456
+ uniqueIndex,
457
+ index
458
+ } from "drizzle-orm/pg-core";
459
+ var snapshots = pgTable(
460
+ "snapshots",
461
+ {
462
+ id: serial("id").primaryKey(),
463
+ entityId: varchar("entity_id", { length: 50 }).notNull(),
464
+ source: varchar("source", { length: 20 }),
465
+ timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
466
+ avgScore: real("avg_score").notNull(),
467
+ positiveCount: integer("positive_count").notNull(),
468
+ negativeCount: integer("negative_count").notNull(),
469
+ neutralCount: integer("neutral_count").notNull(),
470
+ totalCount: integer("total_count").notNull()
471
+ },
472
+ (table) => [
473
+ index("idx_snapshots_entity_time").on(table.entityId, table.timestamp),
474
+ index("idx_snapshots_source").on(
475
+ table.entityId,
476
+ table.source,
477
+ table.timestamp
478
+ )
479
+ ]
480
+ );
481
+ var posts = pgTable(
482
+ "posts",
483
+ {
484
+ id: serial("id").primaryKey(),
485
+ externalId: varchar("external_id", { length: 100 }).notNull(),
486
+ source: varchar("source", { length: 20 }).notNull(),
487
+ entityId: varchar("entity_id", { length: 50 }).notNull(),
488
+ text: text("text").notNull(),
489
+ authorId: varchar("author_id", { length: 100 }),
490
+ authorUsername: varchar("author_username", { length: 100 }),
491
+ url: text("url"),
492
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
493
+ fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull().defaultNow(),
494
+ upvotes: integer("upvotes").default(0),
495
+ comments: integer("comments").default(0),
496
+ shares: integer("shares").default(0),
497
+ sentimentLabel: varchar("sentiment_label", { length: 10 }).notNull(),
498
+ sentimentConfidence: real("sentiment_confidence").notNull(),
499
+ modelId: varchar("model_id", { length: 200 }).notNull(),
500
+ rawResponse: jsonb("raw_response")
501
+ },
502
+ (table) => [
503
+ uniqueIndex("idx_posts_dedup").on(table.externalId, table.source),
504
+ index("idx_posts_entity_time").on(table.entityId, table.createdAt),
505
+ index("idx_posts_source").on(table.source, table.createdAt),
506
+ index("idx_posts_sentiment").on(table.entityId, table.sentimentLabel)
507
+ ]
508
+ );
509
+
510
+ // ../core/src/db/store.ts
511
+ import { desc, eq } from "drizzle-orm";
512
+
513
+ // src/hooks/use-live-data.ts
514
+ var POLL_INTERVAL = 30 * 6e4;
515
+ function useLiveData(apiUrl) {
516
+ const [data, setData] = useState2([]);
517
+ const [loading, setLoading] = useState2(true);
518
+ const [error, setError] = useState2(null);
519
+ const [lastUpdated, setLastUpdated] = useState2(null);
520
+ const fetchViaApi = useCallback(async () => {
521
+ if (!apiUrl) return;
522
+ try {
523
+ const res = await fetch(`${apiUrl}/api/compare`, {
524
+ method: "POST",
525
+ headers: { "Content-Type": "application/json" },
526
+ body: JSON.stringify({
527
+ entityIds: DEFAULT_ENTITIES.map((e) => e.id),
528
+ count: 10
529
+ })
530
+ });
531
+ const json = await res.json();
532
+ if (json.success && json.data) {
533
+ setData(json.data);
534
+ setLastUpdated(/* @__PURE__ */ new Date());
535
+ setError(null);
536
+ } else {
537
+ setError(json.error ?? "API error");
538
+ }
539
+ } catch (err) {
540
+ setError(err instanceof Error ? err.message : "Fetch failed");
541
+ } finally {
542
+ setLoading(false);
543
+ }
544
+ }, [apiUrl]);
545
+ const fetchDirect = useCallback(async () => {
546
+ try {
547
+ const results = [];
548
+ for (const entity of DEFAULT_ENTITIES) {
549
+ const allClassified = [];
550
+ for (const sq of entity.queries) {
551
+ const provider = getProvider(sq.source);
552
+ const posts2 = await provider.fetchPosts(sq.query, { count: 10 });
553
+ const classified = await classifyPosts(posts2, provider.getModelId());
554
+ allClassified.push(...classified);
555
+ }
556
+ const snapshot = aggregate(allClassified);
557
+ results.push({
558
+ entity,
559
+ snapshot,
560
+ posts: allClassified,
561
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
562
+ });
563
+ }
564
+ setData(results);
565
+ setLastUpdated(/* @__PURE__ */ new Date());
566
+ setError(null);
567
+ } catch (err) {
568
+ setError(err instanceof Error ? err.message : "Analysis failed");
569
+ } finally {
570
+ setLoading(false);
571
+ }
572
+ }, []);
573
+ const fetchData = apiUrl ? fetchViaApi : fetchDirect;
574
+ useEffect2(() => {
575
+ fetchData();
576
+ const interval = setInterval(fetchData, POLL_INTERVAL);
577
+ return () => clearInterval(interval);
578
+ }, [fetchData]);
579
+ return { data, loading, error, lastUpdated, refresh: fetchData };
580
+ }
581
+
582
+ // src/app.tsx
583
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
584
+ function App({ apiUrl }) {
585
+ const { data, loading, error, lastUpdated, refresh } = useLiveData(apiUrl);
586
+ const [activeIndex, setActiveIndex] = useState3(0);
587
+ const [showDetail, setShowDetail] = useState3(true);
588
+ const { exit } = useApp();
589
+ useInput((input, key) => {
590
+ if (input === "q") {
591
+ exit();
592
+ }
593
+ if (input === "r") {
594
+ refresh();
595
+ }
596
+ if (key.upArrow) {
597
+ setActiveIndex((i) => Math.max(0, i - 1));
598
+ }
599
+ if (key.downArrow) {
600
+ setActiveIndex((i) => Math.min(data.length - 1, i + 1));
601
+ }
602
+ if (key.return) {
603
+ setShowDetail((s) => !s);
604
+ }
605
+ });
606
+ const activeAnalysis = data[activeIndex];
607
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
608
+ /* @__PURE__ */ jsx5(Header, { lastUpdated, loading }),
609
+ error && /* @__PURE__ */ jsx5(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx5(Text5, { children: chalk5.red(`Error: ${error}`) }) }),
610
+ loading && data.length === 0 ? /* @__PURE__ */ jsx5(Box5, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx5(Text5, { children: chalk5.yellow("\u27F3 Fetching sentiment data...") }) }) : /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, children: [
611
+ /* @__PURE__ */ jsx5(Box5, { marginY: 1, children: /* @__PURE__ */ jsx5(Text5, { children: chalk5.dim(
612
+ " " + "ENTITY".padEnd(COL.name) + "SCORE".padStart(COL.score) + " " + "TREND".padEnd(COL.trend) + "POS".padStart(COL.stat) + "NEU".padStart(COL.stat) + "NEG".padStart(COL.stat) + "TOT".padStart(COL.stat)
613
+ ) }) }),
614
+ /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { children: chalk5.dim(" " + "\u2500".repeat(COL.name + COL.score + 2 + COL.trend + COL.stat * 4)) }) }),
615
+ data.map((analysis, i) => /* @__PURE__ */ jsx5(
616
+ EntityRow,
617
+ {
618
+ analysis,
619
+ isActive: i === activeIndex
620
+ },
621
+ analysis.entity.id
622
+ ))
623
+ ] }),
624
+ showDetail && activeAnalysis && /* @__PURE__ */ jsx5(DetailPanel, { analysis: activeAnalysis }),
625
+ /* @__PURE__ */ jsx5(StatusBar, {})
626
+ ] });
627
+ }
628
+
629
+ // src/cli.tsx
630
+ import { jsx as jsx6 } from "react/jsx-runtime";
631
+ var cli = meow(
632
+ `
633
+ Usage
634
+ $ pythx
635
+
636
+ Options
637
+ --api-url URL of the Pythx web API (default: direct mode, no server needed)
638
+
639
+ Examples
640
+ $ pythx
641
+ $ pythx --api-url http://localhost:3000
642
+ $ pythx --api-url https://pythx.vercel.app
643
+ `,
644
+ {
645
+ importMeta: import.meta,
646
+ flags: {
647
+ apiUrl: {
648
+ type: "string"
649
+ }
650
+ }
651
+ }
652
+ );
653
+ process.stdout.write("\x1B[?1049h");
654
+ process.stdout.write("\x1B[H");
655
+ var instance = render(/* @__PURE__ */ jsx6(App, { apiUrl: cli.flags.apiUrl }), { patchConsole: false });
656
+ instance.waitUntilExit().then(() => {
657
+ process.stdout.write("\x1B[?1049l");
658
+ });
package/package.json CHANGED
@@ -1,7 +1,38 @@
1
1
  {
2
2
  "name": "pythx-cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Real-time sentiment intelligence terminal for prediction markets",
5
+ "type": "module",
6
+ "bin": {
7
+ "pythx": "./bin/pythx.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin"
12
+ ],
13
+ "scripts": {
14
+ "dev": "tsx src/cli.tsx",
15
+ "build": "tsup",
16
+ "lint": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@huggingface/inference": "^4.13.0",
20
+ "drizzle-orm": "^0.45.1",
21
+ "postgres": "^3.4.8",
22
+ "ink": "^5.2.0",
23
+ "react": "^18.3.1",
24
+ "chalk": "^5.4.0",
25
+ "meow": "^13.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@pythx/core": "workspace:*",
29
+ "@types/node": "^22.0.0",
30
+ "@types/react": "^18.3.0",
31
+ "typescript": "^5.8.0",
32
+ "tsx": "^4.19.0",
33
+ "tsup": "^8.0.0",
34
+ "ink-testing-library": "^4.0.0"
35
+ },
5
36
  "keywords": [
6
37
  "sentiment",
7
38
  "terminal",
@@ -11,5 +42,8 @@
11
42
  "x-api"
12
43
  ],
13
44
  "author": "ZachJxyz",
14
- "license": "UNLICENSED"
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/zachjxyz/pythx"
48
+ }
15
49
  }
package/README.md DELETED
@@ -1,5 +0,0 @@
1
- # pythx
2
-
3
- Real-time sentiment intelligence terminal.
4
-
5
- Coming soon.