@spfunctions/cli 1.7.4 → 1.7.7

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/client.d.ts CHANGED
@@ -27,6 +27,22 @@ export declare class SFClient {
27
27
  createStrategyAPI(id: string, data: Record<string, unknown>): Promise<any>;
28
28
  updateStrategyAPI(thesisId: string, strategyId: string, data: Record<string, unknown>): Promise<any>;
29
29
  deleteStrategyAPI(thesisId: string, strategyId: string): Promise<any>;
30
+ searchX(query: string, opts?: {
31
+ mode?: string;
32
+ hours?: number;
33
+ limit?: number;
34
+ }): Promise<any>;
35
+ getXVolume(query: string, opts?: {
36
+ hours?: number;
37
+ granularity?: string;
38
+ }): Promise<any>;
39
+ searchXNews(query: string, opts?: {
40
+ limit?: number;
41
+ }): Promise<any>;
42
+ getXAccount(username: string, opts?: {
43
+ hours?: number;
44
+ limit?: number;
45
+ }): Promise<any>;
30
46
  }
31
47
  export declare function kalshiFetchAllSeries(): Promise<any[]>;
32
48
  export declare function kalshiFetchEvents(seriesTicker: string): Promise<any[]>;
package/dist/client.js CHANGED
@@ -107,6 +107,28 @@ class SFClient {
107
107
  async deleteStrategyAPI(thesisId, strategyId) {
108
108
  return this.request('DELETE', `/api/thesis/${thesisId}/strategies/${strategyId}`);
109
109
  }
110
+ // ── X (Twitter) operations ──
111
+ async searchX(query, opts) {
112
+ const mode = opts?.mode || 'summary';
113
+ const hours = opts?.hours || 24;
114
+ const limit = opts?.limit || 20;
115
+ return this.request('GET', `/api/x/search?q=${encodeURIComponent(query)}&mode=${mode}&hours=${hours}&limit=${limit}`);
116
+ }
117
+ async getXVolume(query, opts) {
118
+ const hours = opts?.hours || 72;
119
+ const granularity = opts?.granularity || 'hour';
120
+ return this.request('GET', `/api/x/volume?q=${encodeURIComponent(query)}&hours=${hours}&granularity=${granularity}`);
121
+ }
122
+ async searchXNews(query, opts) {
123
+ const limit = opts?.limit || 10;
124
+ return this.request('GET', `/api/x/news?q=${encodeURIComponent(query)}&limit=${limit}`);
125
+ }
126
+ async getXAccount(username, opts) {
127
+ const user = username.replace(/^@/, '');
128
+ const hours = opts?.hours || 24;
129
+ const limit = opts?.limit || 20;
130
+ return this.request('GET', `/api/x/account?username=${user}&hours=${hours}&limit=${limit}`);
131
+ }
110
132
  }
111
133
  exports.SFClient = SFClient;
112
134
  // ===== Kalshi Public API (no auth) =====
@@ -770,6 +770,17 @@ async function agentCommand(thesisId, opts) {
770
770
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
771
771
  },
772
772
  },
773
+ {
774
+ name: 'get_markets',
775
+ label: 'Traditional Markets',
776
+ description: 'Get traditional market prices via Databento: S&P 500 (SPY), VIX (VIXY), 20Y Treasury (TLT), Gold (GLD), Oil (USO). Daily close + 1-day change.',
777
+ parameters: emptyParams,
778
+ execute: async () => {
779
+ const { fetchTraditionalMarkets } = await import('../client.js');
780
+ const data = await fetchTraditionalMarkets();
781
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
782
+ },
783
+ },
773
784
  {
774
785
  name: 'inject_signal',
775
786
  label: 'Inject Signal',
@@ -1490,6 +1501,57 @@ async function agentCommand(thesisId, opts) {
1490
1501
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
1491
1502
  },
1492
1503
  });
1504
+ // ── X (Twitter) tools ─────────────────────────────────────────────────────
1505
+ tools.push({
1506
+ name: 'search_x',
1507
+ label: 'X Search',
1508
+ description: 'Search X (Twitter) for recent discussions on a topic. Returns top posts with engagement metrics, sentiment analysis, and key themes. Use for social signal research on any prediction market topic.',
1509
+ parameters: Type.Object({
1510
+ query: Type.String({ description: 'Search query (e.g. "iran oil", "fed rate cut", "$BTC")' }),
1511
+ mode: Type.Optional(Type.String({ description: '"summary" (default, with AI analysis) or "raw" (just posts)' })),
1512
+ hours: Type.Optional(Type.Number({ description: 'Hours of history (default 24)' })),
1513
+ }),
1514
+ execute: async (_toolCallId, params) => {
1515
+ const data = await sfClient.searchX(params.query, { mode: params.mode, hours: params.hours });
1516
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
1517
+ },
1518
+ }, {
1519
+ name: 'x_volume',
1520
+ label: 'X Volume',
1521
+ description: 'Get X discussion volume trend for a topic — total posts, velocity change vs prior period, peak time, and hourly timeseries. Use to detect social momentum shifts.',
1522
+ parameters: Type.Object({
1523
+ query: Type.String({ description: 'Search query' }),
1524
+ hours: Type.Optional(Type.Number({ description: 'Hours of history (default 72)' })),
1525
+ }),
1526
+ execute: async (_toolCallId, params) => {
1527
+ const data = await sfClient.getXVolume(params.query, { hours: params.hours });
1528
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
1529
+ },
1530
+ }, {
1531
+ name: 'x_news',
1532
+ label: 'X News',
1533
+ description: 'Get news stories trending on X — titles, summaries, categories, and ticker mentions. Use for breaking news and narrative tracking.',
1534
+ parameters: Type.Object({
1535
+ query: Type.String({ description: 'Search query' }),
1536
+ limit: Type.Optional(Type.Number({ description: 'Max stories (default 10)' })),
1537
+ }),
1538
+ execute: async (_toolCallId, params) => {
1539
+ const data = await sfClient.searchXNews(params.query, { limit: params.limit });
1540
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
1541
+ },
1542
+ }, {
1543
+ name: 'x_account',
1544
+ label: 'X Account',
1545
+ description: 'Get recent posts from a specific X account. Use to track key opinion leaders, officials, or analysts.',
1546
+ parameters: Type.Object({
1547
+ username: Type.String({ description: 'X username (with or without @)' }),
1548
+ hours: Type.Optional(Type.Number({ description: 'Hours of history (default 24)' })),
1549
+ }),
1550
+ execute: async (_toolCallId, params) => {
1551
+ const data = await sfClient.getXAccount(params.username, { hours: params.hours });
1552
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
1553
+ },
1554
+ });
1493
1555
  // ── Trading tools (conditional on tradingEnabled) ──────────────────────────
1494
1556
  const config = (0, config_js_1.loadConfig)();
1495
1557
  if (config.tradingEnabled) {
@@ -1622,9 +1684,12 @@ When discussing an edge, always state contract expiry and next identifiable cata
1622
1684
 
1623
1685
  ### Research workflow
1624
1686
  For complex questions, chain multiple tool calls:
1625
- 1. get_context 2. inspect_book 3. get_liquidity 4. web_search 5. synthesize
1687
+ 1. get_context 2. inspect_book 3. get_liquidity 4. web_search 5. search_x 6. synthesize
1626
1688
  Don't answer a complex question with a single tool call.
1627
1689
 
1690
+ ### Social signal research
1691
+ Use search_x to check X/Twitter sentiment on any topic — especially useful for geopolitical events, macro shifts, and breaking news that moves prediction markets. Use x_volume to detect discussion spikes (velocity > 1 = increasing attention). Use x_account to track specific analysts or officials.
1692
+
1628
1693
  ### Conditional rules
1629
1694
  - Portfolio/positions questions: flag correlated exposure — positions sharing upstream causal nodes are not independent bets.
1630
1695
  - No catalyst visible within 30 days + edge not improving: flag "stale capital risk."
@@ -2584,6 +2649,16 @@ async function runPlainTextAgent(params) {
2584
2649
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
2585
2650
  },
2586
2651
  },
2652
+ {
2653
+ name: 'get_markets', label: 'Traditional Markets',
2654
+ description: 'Traditional market prices via Databento: SPY, VIX, Treasury, Gold, Oil. Daily close + change.',
2655
+ parameters: emptyParams,
2656
+ execute: async () => {
2657
+ const { fetchTraditionalMarkets } = await import('../client.js');
2658
+ const data = await fetchTraditionalMarkets();
2659
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
2660
+ },
2661
+ },
2587
2662
  {
2588
2663
  name: 'inject_signal', label: 'Inject Signal',
2589
2664
  description: 'Inject a signal into the thesis',
@@ -3169,6 +3244,53 @@ async function runPlainTextAgent(params) {
3169
3244
  },
3170
3245
  },
3171
3246
  ];
3247
+ // ── X (Twitter) tools for plain mode ──────────────────────────────────────
3248
+ tools.push({
3249
+ name: 'search_x', label: 'X Search',
3250
+ description: 'Search X (Twitter) for recent discussions. Returns posts, sentiment, themes.',
3251
+ parameters: Type.Object({
3252
+ query: Type.String({ description: 'Search query' }),
3253
+ mode: Type.Optional(Type.String({ description: '"summary" or "raw"' })),
3254
+ hours: Type.Optional(Type.Number({ description: 'Hours (default 24)' })),
3255
+ }),
3256
+ execute: async (_id, p) => {
3257
+ const data = await sfClient.searchX(p.query, { mode: p.mode, hours: p.hours });
3258
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
3259
+ },
3260
+ }, {
3261
+ name: 'x_volume', label: 'X Volume',
3262
+ description: 'X discussion volume trend — total posts, velocity, peak time, timeseries.',
3263
+ parameters: Type.Object({
3264
+ query: Type.String({ description: 'Search query' }),
3265
+ hours: Type.Optional(Type.Number({ description: 'Hours (default 72)' })),
3266
+ }),
3267
+ execute: async (_id, p) => {
3268
+ const data = await sfClient.getXVolume(p.query, { hours: p.hours });
3269
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
3270
+ },
3271
+ }, {
3272
+ name: 'x_news', label: 'X News',
3273
+ description: 'News stories trending on X — titles, summaries, tickers.',
3274
+ parameters: Type.Object({
3275
+ query: Type.String({ description: 'Search query' }),
3276
+ limit: Type.Optional(Type.Number({ description: 'Max stories (default 10)' })),
3277
+ }),
3278
+ execute: async (_id, p) => {
3279
+ const data = await sfClient.searchXNews(p.query, { limit: p.limit });
3280
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
3281
+ },
3282
+ }, {
3283
+ name: 'x_account', label: 'X Account',
3284
+ description: 'Recent posts from a specific X account.',
3285
+ parameters: Type.Object({
3286
+ username: Type.String({ description: 'X username (with or without @)' }),
3287
+ hours: Type.Optional(Type.Number({ description: 'Hours (default 24)' })),
3288
+ }),
3289
+ execute: async (_id, p) => {
3290
+ const data = await sfClient.getXAccount(p.username, { hours: p.hours });
3291
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
3292
+ },
3293
+ });
3172
3294
  // ── Trading tools (conditional on tradingEnabled) for plain mode ──────────
3173
3295
  const config = (0, config_js_1.loadConfig)();
3174
3296
  if (config.tradingEnabled) {
@@ -3270,7 +3392,8 @@ Kill conditions: each causal node has a falsifier. Check these first when evalua
3270
3392
 
3271
3393
  Always state contract expiry and next catalyst. No catalyst = flag capital lock risk.
3272
3394
 
3273
- For complex questions, chain: get_context -> inspect_book -> get_liquidity -> web_search -> synthesize.
3395
+ For complex questions, chain: get_context -> inspect_book -> get_liquidity -> web_search -> search_x -> synthesize.
3396
+ Use search_x for social sentiment on any topic. Use x_volume to detect discussion spikes. Use x_account to track key people.
3274
3397
 
3275
3398
  Flag correlated exposure across positions sharing upstream nodes. If nothing to do, say so.
3276
3399
 
@@ -49,16 +49,29 @@ async function queryCommand(q, opts) {
49
49
  }
50
50
  console.log();
51
51
  }
52
- // Markets
53
- if (data.markets?.length > 0) {
52
+ // Markets — show each venue separately
53
+ const formatMarket = (m) => {
54
+ const venue = m.venue === 'kalshi' ? '\x1b[36mK\x1b[39m' : '\x1b[35mP\x1b[39m';
55
+ const vol = m.volume > 1_000_000 ? `${(m.volume / 1_000_000).toFixed(1)}M` :
56
+ m.volume > 1_000 ? `${(m.volume / 1_000).toFixed(0)}K` :
57
+ String(Math.round(m.volume));
58
+ const ticker = m.ticker ? ` ${m.ticker}` : '';
59
+ console.log(` ${venue} ${String(m.price).padStart(3)}¢ vol ${vol.padStart(6)} ${m.title.slice(0, 55)}${ticker}`);
60
+ };
61
+ const kalshiList = data.kalshi || [];
62
+ const polyList = data.polymarket || [];
63
+ const hasMarkets = kalshiList.length > 0 || polyList.length > 0;
64
+ if (hasMarkets) {
54
65
  console.log(' \x1b[1mMarkets\x1b[22m');
55
- for (const m of data.markets.slice(0, 8)) {
56
- const venue = m.venue === 'kalshi' ? '\x1b[36mK\x1b[39m' : '\x1b[35mP\x1b[39m';
57
- const vol = m.volume > 1_000_000 ? `${(m.volume / 1_000_000).toFixed(1)}M` :
58
- m.volume > 1_000 ? `${(m.volume / 1_000).toFixed(0)}K` :
59
- String(m.volume);
60
- const ticker = m.ticker ? ` ${m.ticker}` : '';
61
- console.log(` ${venue} ${String(m.price).padStart(3)}¢ vol ${vol.padStart(6)} ${m.title.slice(0, 55)}${ticker}`);
66
+ if (kalshiList.length > 0) {
67
+ for (const m of kalshiList.slice(0, 8))
68
+ formatMarket(m);
69
+ }
70
+ if (polyList.length > 0) {
71
+ if (kalshiList.length > 0)
72
+ console.log(); // separator between venues
73
+ for (const m of polyList.slice(0, 8))
74
+ formatMarket(m);
62
75
  }
63
76
  console.log();
64
77
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * sf x "query" — Search X discussions
3
+ * sf x-volume "query" — Discussion volume trend
4
+ * sf x-news "query" — X news stories
5
+ * sf x-account @user — Track account posts
6
+ *
7
+ * All use authenticated /api/x/* endpoints via SFClient.
8
+ */
9
+ export declare function xSearchCommand(query: string, opts?: {
10
+ json?: boolean;
11
+ raw?: boolean;
12
+ hours?: string;
13
+ limit?: string;
14
+ }): Promise<void>;
15
+ export declare function xVolumeCommand(query: string, opts?: {
16
+ json?: boolean;
17
+ hours?: string;
18
+ granularity?: string;
19
+ }): Promise<void>;
20
+ export declare function xNewsCommand(query: string, opts?: {
21
+ json?: boolean;
22
+ limit?: string;
23
+ }): Promise<void>;
24
+ export declare function xAccountCommand(username: string, opts?: {
25
+ json?: boolean;
26
+ hours?: string;
27
+ limit?: string;
28
+ }): Promise<void>;
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * sf x "query" — Search X discussions
4
+ * sf x-volume "query" — Discussion volume trend
5
+ * sf x-news "query" — X news stories
6
+ * sf x-account @user — Track account posts
7
+ *
8
+ * All use authenticated /api/x/* endpoints via SFClient.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.xSearchCommand = xSearchCommand;
12
+ exports.xVolumeCommand = xVolumeCommand;
13
+ exports.xNewsCommand = xNewsCommand;
14
+ exports.xAccountCommand = xAccountCommand;
15
+ const BASE_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
16
+ // ── Helpers ──────────────────────────────────────────────────────────────────
17
+ function getApiKey() {
18
+ const key = process.env.SF_API_KEY || '';
19
+ if (!key) {
20
+ console.error(' API key required. Run: sf setup');
21
+ process.exit(1);
22
+ }
23
+ return key;
24
+ }
25
+ async function apiFetch(path) {
26
+ const res = await fetch(`${BASE_URL}${path}`, {
27
+ headers: { 'Authorization': `Bearer ${getApiKey()}` },
28
+ });
29
+ if (!res.ok) {
30
+ const text = await res.text();
31
+ throw new Error(`${res.status}: ${text}`);
32
+ }
33
+ return res.json();
34
+ }
35
+ // ── sf x "query" ─────────────────────────────────────────────────────────────
36
+ async function xSearchCommand(query, opts) {
37
+ const mode = opts?.raw ? 'raw' : 'summary';
38
+ const hours = opts?.hours || '24';
39
+ const limit = opts?.limit || '20';
40
+ const data = await apiFetch(`/api/x/search?q=${encodeURIComponent(query)}&mode=${mode}&hours=${hours}&limit=${limit}`);
41
+ if (opts?.json) {
42
+ console.log(JSON.stringify(data, null, 2));
43
+ return;
44
+ }
45
+ // Summary
46
+ if (data.summary) {
47
+ console.log(`\n \x1b[1m\x1b[36mX: ${query}\x1b[0m\n`);
48
+ console.log(` ${data.summary}\n`);
49
+ if (data.sentiment) {
50
+ const colors = { bullish: '\x1b[32m', bearish: '\x1b[31m', mixed: '\x1b[33m', neutral: '\x1b[2m' };
51
+ console.log(` Sentiment: ${colors[data.sentiment] || ''}${data.sentiment}\x1b[0m`);
52
+ }
53
+ if (data.keyThemes?.length) {
54
+ console.log(` Themes: ${data.keyThemes.join(', ')}`);
55
+ }
56
+ console.log();
57
+ }
58
+ // Posts
59
+ const posts = data.posts || [];
60
+ if (posts.length > 0) {
61
+ console.log(` \x1b[1mTop posts\x1b[22m (${posts.length})\n`);
62
+ for (const p of posts.slice(0, 10)) {
63
+ const likes = p.metrics?.likes || 0;
64
+ const rt = p.metrics?.retweets || 0;
65
+ const followers = p.authorFollowers ? ` (${formatNum(p.authorFollowers)})` : '';
66
+ console.log(` \x1b[36m@${p.authorUsername}\x1b[0m${followers}`);
67
+ console.log(` ${p.text.slice(0, 140)}${p.text.length > 140 ? '...' : ''}`);
68
+ console.log(` \x1b[2m♥ ${likes} ↻ ${rt}\x1b[0m\n`);
69
+ }
70
+ }
71
+ // Volume
72
+ if (data.volume) {
73
+ const v = data.volume;
74
+ const vel = v.velocityChange > 1
75
+ ? `\x1b[32m+${Math.round((v.velocityChange - 1) * 100)}%\x1b[0m`
76
+ : `\x1b[31m${Math.round((v.velocityChange - 1) * 100)}%\x1b[0m`;
77
+ console.log(` \x1b[2mVolume: ${formatNum(v.totalCount)} posts (${hours}h) | Velocity: ${vel} | Peak: ${v.peak?.count}\x1b[0m\n`);
78
+ }
79
+ // News
80
+ const news = data.news || [];
81
+ if (news.length > 0) {
82
+ console.log(` \x1b[1mX News\x1b[22m (${news.length})\n`);
83
+ for (const n of news.slice(0, 5)) {
84
+ const tickers = n.tickers?.length ? ` \x1b[33m${n.tickers.join(' ')}\x1b[0m` : '';
85
+ console.log(` ${n.title}${tickers}`);
86
+ console.log(` \x1b[2m${n.summary.slice(0, 120)}...\x1b[0m\n`);
87
+ }
88
+ }
89
+ }
90
+ // ── sf x-volume "query" ──────────────────────────────────────────────────────
91
+ async function xVolumeCommand(query, opts) {
92
+ const hours = opts?.hours || '72';
93
+ const granularity = opts?.granularity || 'hour';
94
+ const data = await apiFetch(`/api/x/volume?q=${encodeURIComponent(query)}&hours=${hours}&granularity=${granularity}`);
95
+ if (opts?.json) {
96
+ console.log(JSON.stringify(data, null, 2));
97
+ return;
98
+ }
99
+ console.log(`\n \x1b[1m\x1b[36mX Volume: ${query}\x1b[0m\n`);
100
+ console.log(` Total: ${formatNum(data.totalCount)} posts (${hours}h)`);
101
+ const vel = data.velocityChange > 1
102
+ ? `\x1b[32m+${Math.round((data.velocityChange - 1) * 100)}%\x1b[0m`
103
+ : `\x1b[31m${Math.round((data.velocityChange - 1) * 100)}%\x1b[0m`;
104
+ console.log(` Velocity: ${vel}`);
105
+ console.log(` Peak: ${data.peak?.count} at ${data.peak?.time?.slice(11, 16) || '?'}\n`);
106
+ // Histogram
107
+ const ts = data.timeseries || [];
108
+ if (ts.length > 0) {
109
+ const maxCount = Math.max(...ts.map((b) => b.count));
110
+ const barWidth = 40;
111
+ for (const b of ts.slice(-24)) {
112
+ const t = (b.time || '').slice(11, 16) || '?';
113
+ const w = maxCount > 0 ? Math.round((b.count / maxCount) * barWidth) : 0;
114
+ const bar = '▓'.repeat(w);
115
+ const count = String(b.count).padStart(6);
116
+ console.log(` ${t} ${count} ${bar}`);
117
+ }
118
+ }
119
+ console.log();
120
+ }
121
+ // ── sf x-news "query" ────────────────────────────────────────────────────────
122
+ async function xNewsCommand(query, opts) {
123
+ const limit = opts?.limit || '10';
124
+ const data = await apiFetch(`/api/x/news?q=${encodeURIComponent(query)}&limit=${limit}`);
125
+ if (opts?.json) {
126
+ console.log(JSON.stringify(data, null, 2));
127
+ return;
128
+ }
129
+ const stories = data.stories || [];
130
+ console.log(`\n \x1b[1m\x1b[36mX News: ${query}\x1b[0m (${stories.length} stories)\n`);
131
+ for (const s of stories) {
132
+ const tickers = s.tickers?.length ? ` \x1b[33m${s.tickers.join(' ')}\x1b[0m` : '';
133
+ const cats = s.categories?.length ? `\x1b[2m[${s.categories.join(', ')}]\x1b[0m` : '';
134
+ console.log(` \x1b[1m${s.title}\x1b[22m${tickers} ${cats}`);
135
+ console.log(` ${s.summary.slice(0, 200)}${s.summary.length > 200 ? '...' : ''}`);
136
+ console.log();
137
+ }
138
+ }
139
+ // ── sf x-account @username ───────────────────────────────────────────────────
140
+ async function xAccountCommand(username, opts) {
141
+ // Strip @ if present
142
+ const user = username.replace(/^@/, '');
143
+ const hours = opts?.hours || '24';
144
+ const limit = opts?.limit || '20';
145
+ const data = await apiFetch(`/api/x/account?username=${user}&hours=${hours}&limit=${limit}`);
146
+ if (opts?.json) {
147
+ console.log(JSON.stringify(data, null, 2));
148
+ return;
149
+ }
150
+ const posts = data.posts || [];
151
+ console.log(`\n \x1b[1m\x1b[36m@${user}\x1b[0m (${posts.length} posts, last ${hours}h)\n`);
152
+ for (const p of posts) {
153
+ const likes = p.metrics?.likes || 0;
154
+ const rt = p.metrics?.retweets || 0;
155
+ const time = p.createdAt?.slice(11, 16) || '';
156
+ console.log(` \x1b[2m${time}\x1b[0m ${p.text.slice(0, 140)}${p.text.length > 140 ? '...' : ''}`);
157
+ console.log(` \x1b[2m♥ ${likes} ↻ ${rt}\x1b[0m\n`);
158
+ }
159
+ }
160
+ // ── Util ─────────────────────────────────────────────────────────────────────
161
+ function formatNum(n) {
162
+ if (n >= 1_000_000)
163
+ return `${(n / 1_000_000).toFixed(1)}M`;
164
+ if (n >= 1_000)
165
+ return `${(n / 1_000).toFixed(1)}K`;
166
+ return String(n);
167
+ }
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ const augment_js_1 = require("./commands/augment.js");
59
59
  const telegram_js_1 = require("./commands/telegram.js");
60
60
  const query_js_1 = require("./commands/query.js");
61
61
  const markets_js_1 = require("./commands/markets.js");
62
+ const x_js_1 = require("./commands/x.js");
62
63
  const utils_js_1 = require("./utils.js");
63
64
  // ── Apply ~/.sf/config.json to process.env BEFORE any command ────────────────
64
65
  // This means client.ts, kalshi.ts, agent.ts keep reading process.env and just work.
@@ -119,6 +120,12 @@ const GROUPED_HELP = `
119
120
  \x1b[36mprompt\x1b[39m [id] Dynamic system prompt for any agent
120
121
  \x1b[36mtelegram\x1b[39m Telegram bot for monitoring
121
122
 
123
+ \x1b[1mX / Twitter\x1b[22m
124
+ \x1b[36mx\x1b[39m "query" Search X discussions (summary + raw mode)
125
+ \x1b[36mx-volume\x1b[39m "query" Discussion volume and velocity trend
126
+ \x1b[36mx-news\x1b[39m "query" X news stories (Grok-aggregated)
127
+ \x1b[36mx-account\x1b[39m @username Recent posts from a specific account
128
+
122
129
  \x1b[1mInfo\x1b[22m
123
130
  \x1b[36mfeed\x1b[39m Evaluation history stream
124
131
  \x1b[36mmilestones\x1b[39m Upcoming Kalshi events
@@ -134,9 +141,13 @@ program
134
141
  .option('--api-url <url>', 'API base URL (or set SF_API_URL env var)')
135
142
  .configureHelp({
136
143
  formatHelp: (cmd, helper) => {
137
- if (cmd.parent)
138
- return helper.formatHelp(cmd, helper);
139
- return GROUPED_HELP;
144
+ // Root command (sf --help) → show grouped help
145
+ // Subcommands (sf query --help) → default behavior
146
+ if (!cmd.parent)
147
+ return GROUPED_HELP;
148
+ const { Help } = require('commander');
149
+ const defaultHelper = new Help();
150
+ return defaultHelper.formatHelp(cmd, helper);
140
151
  },
141
152
  })
142
153
  .action(async () => {
@@ -656,6 +667,46 @@ program
656
667
  .action(async (opts) => {
657
668
  await run(() => (0, telegram_js_1.telegramCommand)(opts));
658
669
  });
670
+ // ── sf x "query" ─────────────────────────────────────────────────────────────
671
+ program
672
+ .command('x <query>')
673
+ .description('Search X/Twitter discussions about a topic')
674
+ .option('--json', 'JSON output')
675
+ .option('--raw', 'Raw mode (no LLM summary)')
676
+ .option('--hours <n>', 'Hours to search back (default 24)', '24')
677
+ .option('--limit <n>', 'Max results (default 20)', '20')
678
+ .action(async (query, opts) => {
679
+ await run(() => (0, x_js_1.xSearchCommand)(query, opts));
680
+ });
681
+ // ── sf x-volume "query" ──────────────────────────────────────────────────────
682
+ program
683
+ .command('x-volume <query>')
684
+ .description('X/Twitter discussion volume and velocity trend')
685
+ .option('--json', 'JSON output')
686
+ .option('--hours <n>', 'Hours to look back (default 72)', '72')
687
+ .option('--granularity <g>', 'minute|hour|day (default hour)', 'hour')
688
+ .action(async (query, opts) => {
689
+ await run(() => (0, x_js_1.xVolumeCommand)(query, opts));
690
+ });
691
+ // ── sf x-news "query" ────────────────────────────────────────────────────────
692
+ program
693
+ .command('x-news <query>')
694
+ .description('X/Twitter news stories (Grok-aggregated)')
695
+ .option('--json', 'JSON output')
696
+ .option('--limit <n>', 'Max stories (default 10)', '10')
697
+ .action(async (query, opts) => {
698
+ await run(() => (0, x_js_1.xNewsCommand)(query, opts));
699
+ });
700
+ // ── sf x-account @username ───────────────────────────────────────────────────
701
+ program
702
+ .command('x-account <username>')
703
+ .description('Recent posts from a specific X/Twitter account')
704
+ .option('--json', 'JSON output')
705
+ .option('--hours <n>', 'Hours to look back (default 24)', '24')
706
+ .option('--limit <n>', 'Max posts (default 20)', '20')
707
+ .action(async (username, opts) => {
708
+ await run(() => (0, x_js_1.xAccountCommand)(username, opts));
709
+ });
659
710
  // ── sf strategies ─────────────────────────────────────────────────────────────
660
711
  (0, strategies_js_1.registerStrategies)(program);
661
712
  // ── Error wrapper ─────────────────────────────────────────────────────────────
@@ -58,6 +58,16 @@ async function buildTools(sfClient, thesisId, latestContext) {
58
58
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
59
59
  },
60
60
  },
61
+ {
62
+ name: 'get_markets', label: 'Markets',
63
+ description: 'Traditional market prices: SPY, VIX, Treasury, Gold, Oil.',
64
+ parameters: emptyParams,
65
+ execute: async () => {
66
+ const { fetchTraditionalMarkets } = await import('../client.js');
67
+ const data = await fetchTraditionalMarkets();
68
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
69
+ },
70
+ },
61
71
  {
62
72
  name: 'inject_signal', label: 'Signal',
63
73
  description: 'Inject a signal (news, note, observation) into the thesis',
@@ -294,6 +304,53 @@ async function buildTools(sfClient, thesisId, latestContext) {
294
304
  },
295
305
  },
296
306
  ];
307
+ // ── X (Twitter) tools ──
308
+ tools.push({
309
+ name: 'search_x', label: 'X Search',
310
+ description: 'Search X (Twitter) for recent discussions. Returns posts, sentiment, themes.',
311
+ parameters: Type.Object({
312
+ query: Type.String({ description: 'Search query' }),
313
+ mode: Type.Optional(Type.String({ description: '"summary" or "raw"' })),
314
+ hours: Type.Optional(Type.Number({ description: 'Hours (default 24)' })),
315
+ }),
316
+ execute: async (_id, p) => {
317
+ const data = await sfClient.searchX(p.query, { mode: p.mode, hours: p.hours });
318
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
319
+ },
320
+ }, {
321
+ name: 'x_volume', label: 'X Volume',
322
+ description: 'X discussion volume trend — total posts, velocity, peak time.',
323
+ parameters: Type.Object({
324
+ query: Type.String({ description: 'Search query' }),
325
+ hours: Type.Optional(Type.Number({ description: 'Hours (default 72)' })),
326
+ }),
327
+ execute: async (_id, p) => {
328
+ const data = await sfClient.getXVolume(p.query, { hours: p.hours });
329
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
330
+ },
331
+ }, {
332
+ name: 'x_news', label: 'X News',
333
+ description: 'News stories trending on X — titles, summaries, tickers.',
334
+ parameters: Type.Object({
335
+ query: Type.String({ description: 'Search query' }),
336
+ limit: Type.Optional(Type.Number({ description: 'Max stories (default 10)' })),
337
+ }),
338
+ execute: async (_id, p) => {
339
+ const data = await sfClient.searchXNews(p.query, { limit: p.limit });
340
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
341
+ },
342
+ }, {
343
+ name: 'x_account', label: 'X Account',
344
+ description: 'Recent posts from a specific X account.',
345
+ parameters: Type.Object({
346
+ username: Type.String({ description: 'X username (with or without @)' }),
347
+ hours: Type.Optional(Type.Number({ description: 'Hours (default 24)' })),
348
+ }),
349
+ execute: async (_id, p) => {
350
+ const data = await sfClient.getXAccount(p.username, { hours: p.hours });
351
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
352
+ },
353
+ });
297
354
  // Trading tools (only if enabled)
298
355
  if (config.tradingEnabled) {
299
356
  tools.push({
@@ -394,6 +451,7 @@ Price: depth >= 500 = consensus, < 100 = unreliable, spread > 5 = noisy.
394
451
  - Keep Telegram messages SHORT — bullet points, no walls of text.
395
452
  - Prices in cents (¢). P&L in dollars ($). Don't re-convert tool output units.
396
453
  - Call tools for fresh data. Never guess prices or P&L from this prompt.
454
+ - Use search_x for X/Twitter sentiment. Use x_volume for discussion spikes. Use x_account to track key people.
397
455
  - You don't know user's positions. Call get_positions before discussing trades.
398
456
  - If user mentions news, inject_signal immediately. Don't ask "should I?"
399
457
  - If user says "evaluate" or "run it", trigger immediately.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.7.4",
3
+ "version": "1.7.7",
4
4
  "description": "Prediction market intelligence CLI. Causal thesis model, 24/7 Kalshi/Polymarket scan, live orderbook, edge detection. Interactive agent mode with tool calling.",
5
5
  "bin": {
6
6
  "sf": "./dist/index.js"