@blockrun/franklin 3.2.3 → 3.3.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 (41) hide show
  1. package/dist/agent/commands.js +30 -1
  2. package/dist/agent/context.js +13 -0
  3. package/dist/agent/permissions.js +3 -3
  4. package/dist/banner.js +61 -75
  5. package/dist/commands/start.js +33 -2
  6. package/dist/events/bridge.d.ts +1 -0
  7. package/dist/events/bridge.js +24 -0
  8. package/dist/events/bus.d.ts +17 -0
  9. package/dist/events/bus.js +55 -0
  10. package/dist/events/types.d.ts +49 -0
  11. package/dist/events/types.js +8 -0
  12. package/dist/learnings/extractor.d.ts +16 -0
  13. package/dist/learnings/extractor.js +234 -0
  14. package/dist/learnings/index.d.ts +3 -0
  15. package/dist/learnings/index.js +2 -0
  16. package/dist/learnings/store.d.ts +15 -0
  17. package/dist/learnings/store.js +130 -0
  18. package/dist/learnings/types.d.ts +24 -0
  19. package/dist/learnings/types.js +7 -0
  20. package/dist/narrative/state.d.ts +30 -0
  21. package/dist/narrative/state.js +69 -0
  22. package/dist/social/browser-pool.d.ts +29 -0
  23. package/dist/social/browser-pool.js +57 -0
  24. package/dist/social/preflight.d.ts +14 -0
  25. package/dist/social/preflight.js +26 -0
  26. package/dist/social/x.d.ts +8 -0
  27. package/dist/social/x.js +9 -1
  28. package/dist/tools/index.js +7 -0
  29. package/dist/tools/posttox.d.ts +7 -0
  30. package/dist/tools/posttox.js +137 -0
  31. package/dist/tools/searchx.d.ts +7 -0
  32. package/dist/tools/searchx.js +111 -0
  33. package/dist/tools/trading.d.ts +3 -0
  34. package/dist/tools/trading.js +168 -0
  35. package/dist/trading/config.d.ts +23 -0
  36. package/dist/trading/config.js +45 -0
  37. package/dist/trading/data.d.ts +30 -0
  38. package/dist/trading/data.js +112 -0
  39. package/dist/trading/metrics.d.ts +29 -0
  40. package/dist/trading/metrics.js +105 -0
  41. package/package.json +1 -1
package/dist/social/x.js CHANGED
@@ -17,6 +17,8 @@ import { SocialBrowser } from './browser.js';
17
17
  import { findRefs, findStaticText, extractArticleBlocks, X_TIME_LINK_PATTERN } from './a11y.js';
18
18
  import { computePreKey, hasPreKey, commitPreKey, hasPosted, countPostedToday, logReply, } from './db.js';
19
19
  import { detectProduct, generateReply } from './ai.js';
20
+ import { bus } from '../events/bus.js';
21
+ import { makeEvent } from '../events/types.js';
20
22
  /**
21
23
  * Main entry point. Iterates every search query in config.x.search_queries
22
24
  * and processes every visible candidate until the daily target is hit.
@@ -202,6 +204,12 @@ export async function runX(opts) {
202
204
  cost_usd: gen.cost,
203
205
  });
204
206
  commitPreKey('x', handle, preKey);
207
+ bus.emit(makeEvent({
208
+ type: 'post.published',
209
+ source: 'social',
210
+ costUsd: gen.cost,
211
+ data: { platform: 'x', url: canonicalUrl, text: gen.reply },
212
+ }));
205
213
  // Respect the rate-limit / anti-spam delay between successes
206
214
  await browser.waitForTimeout(opts.config.x.min_delay_seconds * 1000);
207
215
  }
@@ -238,7 +246,7 @@ export async function runX(opts) {
238
246
  * Enter+Enter), clicks the reply button, confirms the "Your post was sent"
239
247
  * banner.
240
248
  */
241
- async function postReply(browser, reply) {
249
+ export async function postReply(browser, reply) {
242
250
  // Snapshot and find the reply textbox
243
251
  const tree = await browser.snapshot();
244
252
  const boxRefs = findRefs(tree, 'textbox', 'Post (your reply|text).*');
@@ -12,6 +12,9 @@ import { webSearchCapability } from './websearch.js';
12
12
  import { taskCapability } from './task.js';
13
13
  import { imageGenCapability } from './imagegen.js';
14
14
  import { askUserCapability } from './askuser.js';
15
+ import { tradingSignalCapability, tradingMarketCapability } from './trading.js';
16
+ import { searchXCapability } from './searchx.js';
17
+ import { postToXCapability } from './posttox.js';
15
18
  /** All capabilities available to the runcode agent (excluding sub-agent, which needs config). */
16
19
  export const allCapabilities = [
17
20
  readCapability,
@@ -25,6 +28,10 @@ export const allCapabilities = [
25
28
  taskCapability,
26
29
  imageGenCapability,
27
30
  askUserCapability,
31
+ tradingSignalCapability,
32
+ tradingMarketCapability,
33
+ searchXCapability,
34
+ postToXCapability,
28
35
  ];
29
36
  export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
30
37
  export { createSubAgentCapability } from './subagent.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * PostToX capability — post a reply to a tweet on X.
3
+ * The agent MUST confirm the reply text with the user before calling this tool.
4
+ * Requires the pre_key from a SearchX result.
5
+ */
6
+ import type { CapabilityHandler } from '../agent/types.js';
7
+ export declare const postToXCapability: CapabilityHandler;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * PostToX capability — post a reply to a tweet on X.
3
+ * The agent MUST confirm the reply text with the user before calling this tool.
4
+ * Requires the pre_key from a SearchX result.
5
+ */
6
+ import { checkSocialReady } from '../social/preflight.js';
7
+ import { browserPool } from '../social/browser-pool.js';
8
+ import { extractArticleBlocks, findRefs, findStaticText, X_TIME_LINK_PATTERN, } from '../social/a11y.js';
9
+ import { computePreKey, commitPreKey, hasPosted, logReply } from '../social/db.js';
10
+ import { loadConfig } from '../social/config.js';
11
+ import { postReply } from '../social/x.js';
12
+ import { bus } from '../events/bus.js';
13
+ import { makeEvent } from '../events/types.js';
14
+ async function execute(input, _ctx) {
15
+ const { pre_key, reply_text, search_query } = input;
16
+ if (!pre_key || !reply_text || !search_query) {
17
+ return {
18
+ output: 'Error: pre_key, reply_text, and search_query are all required',
19
+ isError: true,
20
+ };
21
+ }
22
+ // ── Preflight: config + login ──────────────────────────────────────────
23
+ const preflight = await checkSocialReady();
24
+ if (!preflight.ready) {
25
+ return {
26
+ output: `PostToX not ready: ${preflight.reason}`,
27
+ isError: true,
28
+ };
29
+ }
30
+ const config = loadConfig();
31
+ const handle = config.handle || 'unknown';
32
+ let browser;
33
+ try {
34
+ browser = await browserPool.getBrowser();
35
+ // ── Navigate to search results to re-find the target post ────────
36
+ const searchUrl = `https://x.com/search?q=${encodeURIComponent(search_query)}&src=typed_query&f=live`;
37
+ await browser.open(searchUrl);
38
+ await browser.waitForTimeout(3500);
39
+ const tree = await browser.snapshot();
40
+ // ── Find the article matching the given pre_key ──────────────────
41
+ const articles = extractArticleBlocks(tree);
42
+ let matchedTimeRef = null;
43
+ for (const article of articles) {
44
+ const timeRefs = findRefs(article.text, 'link', X_TIME_LINK_PATTERN);
45
+ if (timeRefs.length === 0)
46
+ continue;
47
+ const texts = findStaticText(article.text);
48
+ const snippet = texts.slice(0, 3).join(' ').trim();
49
+ if (!snippet)
50
+ continue;
51
+ const timeLinkMatch = new RegExp(`\\[${timeRefs[0]}\\]\\s+link:\\s*(.+)`).exec(article.text);
52
+ const timeText = timeLinkMatch ? timeLinkMatch[1].trim() : '';
53
+ const candidatePreKey = computePreKey({ snippet, time: timeText });
54
+ if (candidatePreKey === pre_key) {
55
+ matchedTimeRef = timeRefs[0];
56
+ break;
57
+ }
58
+ }
59
+ if (!matchedTimeRef) {
60
+ return {
61
+ output: 'Post not found in current results. It may have scrolled off or been deleted.',
62
+ isError: true,
63
+ };
64
+ }
65
+ // ── Click through to the tweet page ──────────────────────────────
66
+ await browser.click(matchedTimeRef);
67
+ await browser.waitForTimeout(3000);
68
+ const canonicalUrl = await browser.getUrl();
69
+ // ── Check if already posted to this URL ──────────────────────────
70
+ if (hasPosted('x', handle, canonicalUrl)) {
71
+ return {
72
+ output: `Already replied to this post: ${canonicalUrl}`,
73
+ isError: true,
74
+ };
75
+ }
76
+ // ── Post the reply ───────────────────────────────────────────────
77
+ await postReply(browser, reply_text);
78
+ // ── Record success ───────────────────────────────────────────────
79
+ commitPreKey('x', handle, pre_key);
80
+ logReply({
81
+ platform: 'x',
82
+ handle,
83
+ post_url: canonicalUrl,
84
+ post_title: '',
85
+ post_snippet: '',
86
+ reply_text,
87
+ status: 'posted',
88
+ });
89
+ // ── Emit post.published event ────────────────────────────────────
90
+ await bus.emit(makeEvent({
91
+ type: 'post.published',
92
+ source: 'social',
93
+ data: {
94
+ platform: 'x',
95
+ url: canonicalUrl,
96
+ text: reply_text,
97
+ },
98
+ }));
99
+ return {
100
+ output: `Reply posted successfully to ${canonicalUrl}`,
101
+ };
102
+ }
103
+ catch (err) {
104
+ const msg = err instanceof Error ? err.message : String(err);
105
+ return { output: `PostToX error: ${msg}`, isError: true };
106
+ }
107
+ finally {
108
+ browserPool.releaseBrowser();
109
+ }
110
+ }
111
+ export const postToXCapability = {
112
+ spec: {
113
+ name: 'PostToX',
114
+ description: 'Post a reply to a tweet on X. The agent MUST confirm the reply text with ' +
115
+ 'the user before calling this tool. Requires the pre_key from a SearchX result.',
116
+ input_schema: {
117
+ type: 'object',
118
+ properties: {
119
+ pre_key: {
120
+ type: 'string',
121
+ description: 'preKey of the target post (from SearchX results)',
122
+ },
123
+ reply_text: {
124
+ type: 'string',
125
+ description: 'The reply text to post',
126
+ },
127
+ search_query: {
128
+ type: 'string',
129
+ description: 'The original search query (to re-find the post)',
130
+ },
131
+ },
132
+ required: ['pre_key', 'reply_text', 'search_query'],
133
+ },
134
+ },
135
+ execute,
136
+ concurrent: false,
137
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * SearchX capability — search X (Twitter) for posts matching a query.
3
+ * Returns candidate posts with snippets and product relevance scores.
4
+ * Requires social config and X login.
5
+ */
6
+ import type { CapabilityHandler } from '../agent/types.js';
7
+ export declare const searchXCapability: CapabilityHandler;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * SearchX capability — search X (Twitter) for posts matching a query.
3
+ * Returns candidate posts with snippets and product relevance scores.
4
+ * Requires social config and X login.
5
+ */
6
+ import { checkSocialReady } from '../social/preflight.js';
7
+ import { extractArticleBlocks, findRefs, findStaticText, X_TIME_LINK_PATTERN, } from '../social/a11y.js';
8
+ import { computePreKey, hasPreKey } from '../social/db.js';
9
+ import { detectProduct } from '../social/ai.js';
10
+ import { loadConfig } from '../social/config.js';
11
+ import { browserPool } from '../social/browser-pool.js';
12
+ async function execute(input, _ctx) {
13
+ const { query, max_results } = input;
14
+ if (!query) {
15
+ return { output: 'Error: query is required', isError: true };
16
+ }
17
+ const maxResults = Math.min(Math.max(max_results ?? 10, 1), 50);
18
+ // ── Preflight: config + login ──────────────────────────────────────────
19
+ const preflight = await checkSocialReady();
20
+ if (!preflight.ready) {
21
+ return {
22
+ output: `SearchX not ready: ${preflight.reason}`,
23
+ isError: true,
24
+ };
25
+ }
26
+ const config = loadConfig();
27
+ const handle = config.handle || 'unknown';
28
+ let browser;
29
+ try {
30
+ browser = await browserPool.getBrowser();
31
+ // ── Navigate to X search ───────────────────────────────────────────
32
+ const searchUrl = `https://x.com/search?q=${encodeURIComponent(query)}&src=typed_query&f=live`;
33
+ await browser.open(searchUrl);
34
+ await browser.waitForTimeout(3500);
35
+ const tree = await browser.snapshot();
36
+ // ── Extract articles ───────────────────────────────────────────────
37
+ const articles = extractArticleBlocks(tree);
38
+ const candidates = [];
39
+ for (const article of articles) {
40
+ if (candidates.length >= maxResults)
41
+ break;
42
+ // Find time-link ref (permalink to the tweet)
43
+ const timeRefs = findRefs(article.text, 'link', X_TIME_LINK_PATTERN);
44
+ if (timeRefs.length === 0)
45
+ continue;
46
+ const timeRef = timeRefs[0];
47
+ // Extract snippet from static text (first 3 lines)
48
+ const texts = findStaticText(article.text);
49
+ const snippet = texts.slice(0, 3).join(' ').trim();
50
+ if (!snippet || snippet.length < 10)
51
+ continue;
52
+ // Extract time text from the ref line
53
+ const timeLinkMatch = new RegExp(`\\[${timeRef}\\]\\s+link:\\s*(.+)`).exec(article.text);
54
+ const timeText = timeLinkMatch ? timeLinkMatch[1].trim() : '';
55
+ // Compute pre-key for dedup
56
+ const preKey = computePreKey({ snippet, time: timeText });
57
+ const alreadySeen = hasPreKey('x', handle, preKey);
58
+ // Product routing (zero-cost keyword score)
59
+ const product = detectProduct(snippet, config.products);
60
+ candidates.push({
61
+ index: candidates.length + 1,
62
+ snippet,
63
+ timeText,
64
+ preKey,
65
+ productMatch: product?.name ?? null,
66
+ alreadySeen,
67
+ });
68
+ }
69
+ // ── Format output ──────────────────────────────────────────────────
70
+ if (candidates.length === 0) {
71
+ return { output: `No candidate posts found for query: "${query}"` };
72
+ }
73
+ const lines = candidates.map((c) => {
74
+ const seen = c.alreadySeen ? ' [SEEN]' : '';
75
+ const product = c.productMatch ? ` | product: ${c.productMatch}` : ' | product: none';
76
+ return (`${c.index}. ${c.snippet.slice(0, 200)}\n` +
77
+ ` time: ${c.timeText} | pre_key: ${c.preKey}${product}${seen}`);
78
+ });
79
+ return {
80
+ output: `SearchX results for "${query}" (${candidates.length} candidates):\n\n` +
81
+ lines.join('\n\n'),
82
+ };
83
+ }
84
+ catch (err) {
85
+ const msg = err instanceof Error ? err.message : String(err);
86
+ return { output: `SearchX error: ${msg}`, isError: true };
87
+ }
88
+ finally {
89
+ browserPool.releaseBrowser();
90
+ }
91
+ }
92
+ export const searchXCapability = {
93
+ spec: {
94
+ name: 'SearchX',
95
+ description: 'Search X (Twitter) for posts matching a query. Returns candidate posts ' +
96
+ 'with snippets and product relevance scores. Requires social config and X login.',
97
+ input_schema: {
98
+ type: 'object',
99
+ properties: {
100
+ query: { type: 'string', description: 'Search query' },
101
+ max_results: {
102
+ type: 'number',
103
+ description: 'Max posts to return (default 10)',
104
+ },
105
+ },
106
+ required: ['query'],
107
+ },
108
+ },
109
+ execute,
110
+ concurrent: false,
111
+ };
@@ -0,0 +1,3 @@
1
+ import type { CapabilityHandler } from '../agent/types.js';
2
+ export declare const tradingSignalCapability: CapabilityHandler;
3
+ export declare const tradingMarketCapability: CapabilityHandler;
@@ -0,0 +1,168 @@
1
+ import { getPrice, getOHLCV, getTrending, getMarketOverview } from '../trading/data.js';
2
+ import { rsi, macd, bollingerBands, volatility } from '../trading/metrics.js';
3
+ import { bus } from '../events/bus.js';
4
+ import { makeEvent } from '../events/types.js';
5
+ function formatUsd(n) {
6
+ if (n >= 1e12)
7
+ return `$${(n / 1e12).toFixed(2)}T`;
8
+ if (n >= 1e9)
9
+ return `$${(n / 1e9).toFixed(2)}B`;
10
+ if (n >= 1e6)
11
+ return `$${(n / 1e6).toFixed(2)}M`;
12
+ if (n >= 1e3)
13
+ return `$${(n / 1e3).toFixed(1)}K`;
14
+ return `$${n.toFixed(2)}`;
15
+ }
16
+ async function executeSignal(input, _ctx) {
17
+ const { ticker, days = 30 } = input;
18
+ if (!ticker) {
19
+ return { output: 'Error: ticker is required', isError: true };
20
+ }
21
+ const upper = ticker.toUpperCase();
22
+ const [priceResult, ohlcvResult] = await Promise.all([
23
+ getPrice(upper),
24
+ getOHLCV(upper, days),
25
+ ]);
26
+ if (typeof priceResult === 'string') {
27
+ return { output: `Error fetching price: ${priceResult}`, isError: true };
28
+ }
29
+ if (typeof ohlcvResult === 'string') {
30
+ return { output: `Error fetching OHLCV: ${ohlcvResult}`, isError: true };
31
+ }
32
+ const { closes } = ohlcvResult;
33
+ const rsiResult = rsi(closes);
34
+ const macdResult = macd(closes);
35
+ const bbResult = bollingerBands(closes);
36
+ const volResult = volatility(closes);
37
+ // Determine overall direction from indicators
38
+ let bullish = 0;
39
+ let bearish = 0;
40
+ if (rsiResult.interpretation === 'oversold')
41
+ bullish++;
42
+ if (rsiResult.interpretation === 'overbought')
43
+ bearish++;
44
+ if (macdResult.trend === 'bullish')
45
+ bullish++;
46
+ if (macdResult.trend === 'bearish')
47
+ bearish++;
48
+ if (bbResult.position === 'below')
49
+ bullish++;
50
+ if (bbResult.position === 'above')
51
+ bearish++;
52
+ const direction = bullish > bearish ? 'bullish' : bearish > bullish ? 'bearish' : 'neutral';
53
+ const confidence = Math.max(bullish, bearish) / 3;
54
+ bus.emit(makeEvent({
55
+ type: 'signal.detected',
56
+ source: 'trading',
57
+ data: {
58
+ asset: upper,
59
+ direction,
60
+ confidence,
61
+ indicators: {
62
+ rsi: rsiResult.value,
63
+ macd: macdResult.macd,
64
+ volatility: volResult.annualized,
65
+ },
66
+ summary: `${upper} ${direction} (confidence ${(confidence * 100).toFixed(0)}%)`,
67
+ },
68
+ }));
69
+ const { price, change24h, marketCap, volume24h } = priceResult;
70
+ const last5 = closes.slice(-5).map(c => c.toFixed(2)).join(', ');
71
+ const output = [
72
+ `## ${upper} Signal Report`,
73
+ '',
74
+ `**Price:** $${price.toLocaleString()} USD (${change24h > 0 ? '+' : ''}${change24h.toFixed(2)}% 24h)`,
75
+ `**Market Cap:** ${formatUsd(marketCap)}`,
76
+ `**24h Volume:** ${formatUsd(volume24h)}`,
77
+ '',
78
+ `### Technical Indicators (${days}d lookback)`,
79
+ `- **RSI(14):** ${rsiResult.value.toFixed(1)} — ${rsiResult.interpretation}`,
80
+ `- **MACD:** ${macdResult.macd.toFixed(4)} / Signal: ${macdResult.signal.toFixed(4)} / Histogram: ${macdResult.histogram.toFixed(4)} — ${macdResult.trend}`,
81
+ `- **Bollinger:** Upper ${bbResult.upper.toFixed(2)} / Middle ${bbResult.middle.toFixed(2)} / Lower ${bbResult.lower.toFixed(2)} — Price ${bbResult.position}`,
82
+ `- **Volatility:** ${(volResult.annualized * 100).toFixed(1)}% annualized — ${volResult.interpretation}`,
83
+ '',
84
+ `### Raw Data`,
85
+ `Closes (last 5): ${last5}`,
86
+ ].join('\n');
87
+ return { output };
88
+ }
89
+ export const tradingSignalCapability = {
90
+ spec: {
91
+ name: 'TradingSignal',
92
+ description: 'Get current price, technical indicators (RSI, MACD, Bollinger Bands, volatility), and a signal summary for a cryptocurrency. Returns raw data for the agent to analyze and interpret.',
93
+ input_schema: {
94
+ type: 'object',
95
+ properties: {
96
+ ticker: { type: 'string', description: 'Cryptocurrency ticker, e.g. "BTC", "ETH"' },
97
+ days: { type: 'number', description: 'Lookback period for indicators. Default: 30' },
98
+ },
99
+ required: ['ticker'],
100
+ },
101
+ },
102
+ execute: executeSignal,
103
+ concurrent: true,
104
+ };
105
+ async function executeMarket(input, _ctx) {
106
+ const { action, ticker } = input;
107
+ if (!action) {
108
+ return { output: 'Error: action is required', isError: true };
109
+ }
110
+ switch (action) {
111
+ case 'price': {
112
+ if (!ticker) {
113
+ return { output: 'Error: ticker is required for price action', isError: true };
114
+ }
115
+ const result = await getPrice(ticker.toUpperCase());
116
+ if (typeof result === 'string') {
117
+ return { output: `Error: ${result}`, isError: true };
118
+ }
119
+ const { price, change24h, marketCap, volume24h } = result;
120
+ return {
121
+ output: `${ticker.toUpperCase()}: $${price.toLocaleString()} (${change24h > 0 ? '+' : ''}${change24h.toFixed(2)}% 24h), Market Cap: ${formatUsd(marketCap)}, Volume: ${formatUsd(volume24h)}`,
122
+ };
123
+ }
124
+ case 'trending': {
125
+ const result = await getTrending();
126
+ if (typeof result === 'string') {
127
+ return { output: `Error: ${result}`, isError: true };
128
+ }
129
+ const lines = result.map((c, i) => `${i + 1}. ${c.name} (${c.symbol.toUpperCase()})${c.marketCapRank ? ` — #${c.marketCapRank}` : ''}`);
130
+ return { output: `Trending coins:\n${lines.join('\n')}` };
131
+ }
132
+ case 'overview': {
133
+ const result = await getMarketOverview();
134
+ if (typeof result === 'string') {
135
+ return { output: `Error: ${result}`, isError: true };
136
+ }
137
+ const header = 'Rank | Coin | Price | 24h Change | Market Cap';
138
+ const sep = '-----|------|-------|------------|----------';
139
+ const rows = result.map((c, i) => `${i + 1} | ${c.name} (${c.symbol.toUpperCase()}) | $${c.price.toLocaleString()} | ${c.change24h > 0 ? '+' : ''}${c.change24h.toFixed(2)}% | ${formatUsd(c.marketCap)}`);
140
+ return { output: `Top 20 by Market Cap:\n${header}\n${sep}\n${rows.join('\n')}` };
141
+ }
142
+ default:
143
+ return { output: `Error: unknown action "${action}". Use: price, trending, overview`, isError: true };
144
+ }
145
+ }
146
+ export const tradingMarketCapability = {
147
+ spec: {
148
+ name: 'TradingMarket',
149
+ description: 'Get cryptocurrency market data: price lookup, trending coins, or market overview (top 20 by market cap).',
150
+ input_schema: {
151
+ type: 'object',
152
+ properties: {
153
+ action: {
154
+ type: 'string',
155
+ enum: ['price', 'trending', 'overview'],
156
+ description: 'What to fetch: price lookup, trending coins, or market overview',
157
+ },
158
+ ticker: {
159
+ type: 'string',
160
+ description: 'Cryptocurrency ticker (required for price action), e.g. "BTC"',
161
+ },
162
+ },
163
+ required: ['action'],
164
+ },
165
+ },
166
+ execute: executeMarket,
167
+ concurrent: true,
168
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Typed config for Franklin's trading subsystem.
3
+ * Stored at ~/.blockrun/trading-config.json. Default written on first run.
4
+ */
5
+ export interface TradingConfig {
6
+ version: 1;
7
+ watchlist: string[];
8
+ signals: {
9
+ rsi_oversold: number;
10
+ rsi_overbought: number;
11
+ };
12
+ model_tier: 'free' | 'cheap' | 'premium';
13
+ }
14
+ export declare const CONFIG_PATH: string;
15
+ /**
16
+ * Load config from disk. If missing, write defaults and return them.
17
+ * Returns the parsed config or throws on malformed JSON.
18
+ */
19
+ export declare function loadTradingConfig(): TradingConfig;
20
+ /**
21
+ * Persist config back to disk.
22
+ */
23
+ export declare function saveTradingConfig(cfg: TradingConfig): void;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Typed config for Franklin's trading subsystem.
3
+ * Stored at ~/.blockrun/trading-config.json. Default written on first run.
4
+ */
5
+ import path from 'node:path';
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ export const CONFIG_PATH = path.join(os.homedir(), '.blockrun', 'trading-config.json');
9
+ const DEFAULT_CONFIG = {
10
+ version: 1,
11
+ watchlist: ['BTC', 'ETH', 'SOL'],
12
+ signals: {
13
+ rsi_oversold: 30,
14
+ rsi_overbought: 70,
15
+ },
16
+ model_tier: 'cheap',
17
+ };
18
+ /**
19
+ * Load config from disk. If missing, write defaults and return them.
20
+ * Returns the parsed config or throws on malformed JSON.
21
+ */
22
+ export function loadTradingConfig() {
23
+ const dir = path.dirname(CONFIG_PATH);
24
+ if (!fs.existsSync(dir))
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ if (!fs.existsSync(CONFIG_PATH)) {
27
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
28
+ return { ...DEFAULT_CONFIG };
29
+ }
30
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
31
+ const parsed = JSON.parse(raw);
32
+ if (parsed.version !== 1) {
33
+ throw new Error(`Unsupported trading config version ${parsed.version} (expected 1)`);
34
+ }
35
+ return parsed;
36
+ }
37
+ /**
38
+ * Persist config back to disk.
39
+ */
40
+ export function saveTradingConfig(cfg) {
41
+ const dir = path.dirname(CONFIG_PATH);
42
+ if (!fs.existsSync(dir))
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
45
+ }
@@ -0,0 +1,30 @@
1
+ export interface PriceData {
2
+ price: number;
3
+ change24h: number;
4
+ volume24h: number;
5
+ marketCap: number;
6
+ }
7
+ export interface OHLCVData {
8
+ closes: number[];
9
+ timestamps: number[];
10
+ }
11
+ export interface TrendingCoin {
12
+ id: string;
13
+ name: string;
14
+ symbol: string;
15
+ marketCapRank: number | null;
16
+ }
17
+ export interface MarketCoin {
18
+ id: string;
19
+ symbol: string;
20
+ name: string;
21
+ price: number;
22
+ change24h: number;
23
+ marketCap: number;
24
+ volume24h: number;
25
+ }
26
+ export declare function resolveId(ticker: string): string;
27
+ export declare function getPrice(ticker: string): Promise<PriceData | string>;
28
+ export declare function getOHLCV(ticker: string, days?: number): Promise<OHLCVData | string>;
29
+ export declare function getTrending(): Promise<TrendingCoin[] | string>;
30
+ export declare function getMarketOverview(): Promise<MarketCoin[] | string>;