@spfunctions/cli 1.7.2 → 1.7.6
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 +16 -0
- package/dist/client.js +22 -0
- package/dist/commands/agent.js +125 -2
- package/dist/commands/markets.d.ts +9 -0
- package/dist/commands/markets.js +34 -0
- package/dist/commands/x.d.ts +28 -0
- package/dist/commands/x.js +167 -0
- package/dist/index.js +58 -1
- package/dist/telegram/agent-bridge.js +58 -0
- package/package.json +1 -1
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) =====
|
package/dist/commands/agent.js
CHANGED
|
@@ -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
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sf markets
|
|
4
|
+
*
|
|
5
|
+
* Traditional market snapshot via Databento (SPY, VIX, TLT, Gold, Oil).
|
|
6
|
+
* No auth required — calls public endpoint.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.marketsCommand = marketsCommand;
|
|
10
|
+
const BASE_URL = 'https://simplefunctions.dev';
|
|
11
|
+
async function marketsCommand(opts) {
|
|
12
|
+
const res = await fetch(`${BASE_URL}/api/public/markets`);
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
console.error(` Error: ${res.status} ${await res.text()}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
if (opts?.json) {
|
|
19
|
+
console.log(JSON.stringify(data, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(`\n \x1b[1mTraditional Markets\x1b[22m \x1b[2m${data.snapshotAt?.slice(0, 10) || ''}\x1b[22m\n`);
|
|
23
|
+
if (!data.markets?.length) {
|
|
24
|
+
console.log(' No data available.\n');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const m of data.markets) {
|
|
28
|
+
const arrow = m.changePct >= 0 ? '\x1b[32m▲\x1b[39m' : '\x1b[31m▼\x1b[39m';
|
|
29
|
+
const chg = m.changePct >= 0 ? `\x1b[32m+${m.changePct}%\x1b[39m` : `\x1b[31m${m.changePct}%\x1b[39m`;
|
|
30
|
+
const price = `$${m.price.toFixed(2)}`;
|
|
31
|
+
console.log(` ${arrow} ${m.symbol.padEnd(5)} ${price.padStart(10)} ${chg.padStart(16)} ${m.name}`);
|
|
32
|
+
}
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
@@ -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
|
@@ -58,6 +58,8 @@ const prompt_js_1 = require("./commands/prompt.js");
|
|
|
58
58
|
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
|
+
const markets_js_1 = require("./commands/markets.js");
|
|
62
|
+
const x_js_1 = require("./commands/x.js");
|
|
61
63
|
const utils_js_1 = require("./utils.js");
|
|
62
64
|
// ── Apply ~/.sf/config.json to process.env BEFORE any command ────────────────
|
|
63
65
|
// This means client.ts, kalshi.ts, agent.ts keep reading process.env and just work.
|
|
@@ -86,6 +88,7 @@ const GROUPED_HELP = `
|
|
|
86
88
|
|
|
87
89
|
\x1b[1mSearch\x1b[22m
|
|
88
90
|
\x1b[36mquery\x1b[39m "question" LLM-enhanced market knowledge search \x1b[2m(no auth)\x1b[22m
|
|
91
|
+
\x1b[36mmarkets\x1b[39m Traditional markets: SPY, VIX, bonds, gold, oil \x1b[2m(no auth)\x1b[22m
|
|
89
92
|
|
|
90
93
|
\x1b[1mMarkets\x1b[22m
|
|
91
94
|
\x1b[36mscan\x1b[39m "keywords" Search Kalshi + Polymarket
|
|
@@ -117,6 +120,12 @@ const GROUPED_HELP = `
|
|
|
117
120
|
\x1b[36mprompt\x1b[39m [id] Dynamic system prompt for any agent
|
|
118
121
|
\x1b[36mtelegram\x1b[39m Telegram bot for monitoring
|
|
119
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
|
+
|
|
120
129
|
\x1b[1mInfo\x1b[22m
|
|
121
130
|
\x1b[36mfeed\x1b[39m Evaluation history stream
|
|
122
131
|
\x1b[36mmilestones\x1b[39m Upcoming Kalshi events
|
|
@@ -221,7 +230,7 @@ async function interactiveEntry() {
|
|
|
221
230
|
console.log();
|
|
222
231
|
}
|
|
223
232
|
// ── Pre-action guard: check configuration ────────────────────────────────────
|
|
224
|
-
const NO_CONFIG_COMMANDS = new Set(['setup', 'help', 'scan', 'explore', 'query', 'context', 'milestones', 'forecast', 'settlements', 'balance', 'orders', 'fills', 'schedule', 'announcements', 'history', 'liquidity', 'book', 'prompt', 'sf']);
|
|
233
|
+
const NO_CONFIG_COMMANDS = new Set(['setup', 'help', 'scan', 'explore', 'query', 'context', 'markets', 'milestones', 'forecast', 'settlements', 'balance', 'orders', 'fills', 'schedule', 'announcements', 'history', 'liquidity', 'book', 'prompt', 'sf']);
|
|
225
234
|
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
226
235
|
const cmdName = actionCommand.name();
|
|
227
236
|
if (NO_CONFIG_COMMANDS.has(cmdName))
|
|
@@ -625,6 +634,14 @@ program
|
|
|
625
634
|
const g = cmd.optsWithGlobals();
|
|
626
635
|
await run(() => (0, augment_js_1.augmentCommand)(thesisId, { dryRun: opts.dryRun, json: opts.json, apiKey: g.apiKey, apiUrl: g.apiUrl }));
|
|
627
636
|
});
|
|
637
|
+
// ── sf markets ───────────────────────────────────────────────────────────────
|
|
638
|
+
program
|
|
639
|
+
.command('markets')
|
|
640
|
+
.description('Traditional market snapshot — SPY, VIX, Treasury, Gold, Oil (no auth)')
|
|
641
|
+
.option('--json', 'JSON output')
|
|
642
|
+
.action(async (opts) => {
|
|
643
|
+
await run(() => (0, markets_js_1.marketsCommand)({ json: opts.json }));
|
|
644
|
+
});
|
|
628
645
|
// ── sf query "question" ──────────────────────────────────────────────────────
|
|
629
646
|
program
|
|
630
647
|
.command('query <question>')
|
|
@@ -646,6 +663,46 @@ program
|
|
|
646
663
|
.action(async (opts) => {
|
|
647
664
|
await run(() => (0, telegram_js_1.telegramCommand)(opts));
|
|
648
665
|
});
|
|
666
|
+
// ── sf x "query" ─────────────────────────────────────────────────────────────
|
|
667
|
+
program
|
|
668
|
+
.command('x <query>')
|
|
669
|
+
.description('Search X/Twitter discussions about a topic')
|
|
670
|
+
.option('--json', 'JSON output')
|
|
671
|
+
.option('--raw', 'Raw mode (no LLM summary)')
|
|
672
|
+
.option('--hours <n>', 'Hours to search back (default 24)', '24')
|
|
673
|
+
.option('--limit <n>', 'Max results (default 20)', '20')
|
|
674
|
+
.action(async (query, opts) => {
|
|
675
|
+
await run(() => (0, x_js_1.xSearchCommand)(query, opts));
|
|
676
|
+
});
|
|
677
|
+
// ── sf x-volume "query" ──────────────────────────────────────────────────────
|
|
678
|
+
program
|
|
679
|
+
.command('x-volume <query>')
|
|
680
|
+
.description('X/Twitter discussion volume and velocity trend')
|
|
681
|
+
.option('--json', 'JSON output')
|
|
682
|
+
.option('--hours <n>', 'Hours to look back (default 72)', '72')
|
|
683
|
+
.option('--granularity <g>', 'minute|hour|day (default hour)', 'hour')
|
|
684
|
+
.action(async (query, opts) => {
|
|
685
|
+
await run(() => (0, x_js_1.xVolumeCommand)(query, opts));
|
|
686
|
+
});
|
|
687
|
+
// ── sf x-news "query" ────────────────────────────────────────────────────────
|
|
688
|
+
program
|
|
689
|
+
.command('x-news <query>')
|
|
690
|
+
.description('X/Twitter news stories (Grok-aggregated)')
|
|
691
|
+
.option('--json', 'JSON output')
|
|
692
|
+
.option('--limit <n>', 'Max stories (default 10)', '10')
|
|
693
|
+
.action(async (query, opts) => {
|
|
694
|
+
await run(() => (0, x_js_1.xNewsCommand)(query, opts));
|
|
695
|
+
});
|
|
696
|
+
// ── sf x-account @username ───────────────────────────────────────────────────
|
|
697
|
+
program
|
|
698
|
+
.command('x-account <username>')
|
|
699
|
+
.description('Recent posts from a specific X/Twitter account')
|
|
700
|
+
.option('--json', 'JSON output')
|
|
701
|
+
.option('--hours <n>', 'Hours to look back (default 24)', '24')
|
|
702
|
+
.option('--limit <n>', 'Max posts (default 20)', '20')
|
|
703
|
+
.action(async (username, opts) => {
|
|
704
|
+
await run(() => (0, x_js_1.xAccountCommand)(username, opts));
|
|
705
|
+
});
|
|
649
706
|
// ── sf strategies ─────────────────────────────────────────────────────────────
|
|
650
707
|
(0, strategies_js_1.registerStrategies)(program);
|
|
651
708
|
// ── 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.
|
|
3
|
+
"version": "1.7.6",
|
|
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"
|