@blockrun/franklin 3.8.8 → 3.8.10

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 (50) hide show
  1. package/dist/agent/error-classifier.js +1 -0
  2. package/dist/agent/llm.d.ts +7 -0
  3. package/dist/agent/llm.js +48 -7
  4. package/dist/agent/loop.js +66 -3
  5. package/dist/agent/permissions.js +2 -2
  6. package/dist/agent/types.d.ts +7 -0
  7. package/dist/banner.js +15 -0
  8. package/dist/commands/start.d.ts +4 -0
  9. package/dist/commands/start.js +72 -2
  10. package/dist/index.js +11 -3
  11. package/dist/panel/html.js +111 -21
  12. package/dist/panel/server.js +15 -4
  13. package/dist/tools/activate.d.ts +29 -0
  14. package/dist/tools/activate.js +96 -0
  15. package/dist/tools/index.js +2 -0
  16. package/dist/tools/tool-categories.d.ts +22 -0
  17. package/dist/tools/tool-categories.js +44 -0
  18. package/dist/tools/trading-execute.d.ts +11 -21
  19. package/dist/tools/trading-execute.js +43 -130
  20. package/dist/tools/trading-views.d.ts +64 -0
  21. package/dist/tools/trading-views.js +115 -0
  22. package/dist/tools/trading.js +86 -7
  23. package/dist/tools/webhook.d.ts +18 -0
  24. package/dist/tools/webhook.js +185 -0
  25. package/dist/trading/data.d.ts +24 -1
  26. package/dist/trading/data.js +67 -102
  27. package/dist/trading/providers/blockrun/client.d.ts +48 -0
  28. package/dist/trading/providers/blockrun/client.js +253 -0
  29. package/dist/trading/providers/blockrun/price.d.ts +24 -0
  30. package/dist/trading/providers/blockrun/price.js +110 -0
  31. package/dist/trading/providers/coingecko/client.d.ts +20 -0
  32. package/dist/trading/providers/coingecko/client.js +87 -0
  33. package/dist/trading/providers/coingecko/markets.d.ts +3 -0
  34. package/dist/trading/providers/coingecko/markets.js +25 -0
  35. package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
  36. package/dist/trading/providers/coingecko/ohlcv.js +29 -0
  37. package/dist/trading/providers/coingecko/price.d.ts +11 -0
  38. package/dist/trading/providers/coingecko/price.js +41 -0
  39. package/dist/trading/providers/coingecko/trending.d.ts +3 -0
  40. package/dist/trading/providers/coingecko/trending.js +22 -0
  41. package/dist/trading/providers/fetcher.d.ts +43 -0
  42. package/dist/trading/providers/fetcher.js +45 -0
  43. package/dist/trading/providers/registry.d.ts +45 -0
  44. package/dist/trading/providers/registry.js +82 -0
  45. package/dist/trading/providers/standard-models.d.ts +94 -0
  46. package/dist/trading/providers/standard-models.js +21 -0
  47. package/dist/trading/providers/telemetry.d.ts +51 -0
  48. package/dist/trading/providers/telemetry.js +115 -0
  49. package/dist/ui/app.js +28 -2
  50. package/package.json +1 -1
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Trading view/formatter helpers.
3
+ *
4
+ * Anything that turns engine state into human/agent-readable markdown
5
+ * belongs here. Split out of `trading-execute.ts` so the tool handlers in
6
+ * `trading-router.ts` stay focused on request handling and the engine
7
+ * stays free of presentation concerns. This mirrors the view/controller
8
+ * separation OpenBB enforces between `standard_models` (data) and the
9
+ * router-side rendering that happens in their MCP layer.
10
+ */
11
+ export function formatUsd(n) {
12
+ const sign = n < 0 ? '-' : '';
13
+ const abs = Math.abs(n);
14
+ return `${sign}$${abs.toFixed(2)}`;
15
+ }
16
+ export function formatPct(n) {
17
+ return `${(n * 100).toFixed(1)}%`;
18
+ }
19
+ export function formatPositionLine(p) {
20
+ const pctReturn = (p.markUsd - p.avgPriceUsd) / p.avgPriceUsd;
21
+ const arrow = p.unrealizedPnlUsd >= 0 ? '↑' : '↓';
22
+ return (`- **${p.symbol}** qty=${p.qty} @ avg ${formatUsd(p.avgPriceUsd)} ` +
23
+ `| mark ${formatUsd(p.markUsd)} ${arrow} ` +
24
+ `| unrealized ${formatUsd(p.unrealizedPnlUsd)} (${formatPct(pctReturn)})`);
25
+ }
26
+ export function formatTradeLine(entry) {
27
+ const when = new Date(entry.timestamp).toISOString().replace('T', ' ').slice(0, 16);
28
+ const side = entry.side.toUpperCase();
29
+ const pnl = entry.realizedPnlUsd === 0 ? '' : ` → realized ${formatUsd(entry.realizedPnlUsd)}`;
30
+ return `- ${when} ${side} ${entry.qty} ${entry.symbol} @ ${formatUsd(entry.priceUsd)}${pnl}`;
31
+ }
32
+ /** Parse a window string ("24h", "7d", "all") into a lower-bound timestamp. */
33
+ export function windowToSince(window, now) {
34
+ const m = /^(\d+)\s*([hdwm])$/i.exec(window.trim());
35
+ if (!m)
36
+ return 0;
37
+ const n = parseInt(m[1], 10);
38
+ switch (m[2].toLowerCase()) {
39
+ case 'h': return now - n * 3_600_000;
40
+ case 'd': return now - n * 86_400_000;
41
+ case 'w': return now - n * 7 * 86_400_000;
42
+ case 'm': return now - n * 30 * 86_400_000;
43
+ default: return 0;
44
+ }
45
+ }
46
+ export function renderPortfolio(snap, riskConfig) {
47
+ const lines = [];
48
+ lines.push('## Portfolio');
49
+ lines.push(`- Cash: ${formatUsd(snap.cashUsd)}`);
50
+ lines.push(`- Equity (cash + positions marked-to-market): ${formatUsd(snap.equityUsd)}`);
51
+ lines.push(`- Unrealized P&L: ${formatUsd(snap.unrealizedPnlUsd)}`);
52
+ lines.push(`- Realized P&L (this session): ${formatUsd(snap.realizedPnlUsd)}`);
53
+ lines.push('');
54
+ if (snap.positions.length === 0) {
55
+ lines.push('_No open positions._');
56
+ }
57
+ else {
58
+ lines.push('### Open positions');
59
+ for (const p of snap.positions)
60
+ lines.push(formatPositionLine(p));
61
+ }
62
+ if (riskConfig) {
63
+ const totalExposure = snap.positions.reduce((a, p) => a + p.qty * p.markUsd, 0);
64
+ lines.push('');
65
+ lines.push('### Risk utilization');
66
+ lines.push(`- Total exposure: ${formatUsd(totalExposure)} / cap ${formatUsd(riskConfig.maxTotalExposureUsd)} ` +
67
+ `(${formatPct(totalExposure / riskConfig.maxTotalExposureUsd)})`);
68
+ }
69
+ return lines.join('\n');
70
+ }
71
+ export function renderOrderFilled(params) {
72
+ const { symbol, fill, portfolio } = params;
73
+ const pos = portfolio.getPosition(symbol);
74
+ return (`## Order filled\n` +
75
+ `- Bought ${fill.qty} ${symbol} @ ${formatUsd(fill.priceUsd)} ` +
76
+ `(fee ${formatUsd(fill.feeUsd)})\n` +
77
+ `- Position now: ${pos ? `${pos.qty} ${symbol} @ avg ${formatUsd(pos.avgPriceUsd)}` : '(none)'}\n` +
78
+ `- Cash remaining: ${formatUsd(portfolio.cashUsd)}`);
79
+ }
80
+ export function renderOrderBlocked(params) {
81
+ return (`## Order blocked\n` +
82
+ `- Symbol: ${params.symbol}\n` +
83
+ `- Attempted: buy ${params.qty} @ ${formatUsd(params.priceUsd)}\n` +
84
+ `- Reason: ${params.reason}\n\n` +
85
+ `Try a smaller qty, or close other positions first to free up exposure headroom.`);
86
+ }
87
+ export function renderPositionClosed(params) {
88
+ const { symbol, fill, tradeRealized, portfolio } = params;
89
+ const remaining = portfolio.getPosition(symbol);
90
+ return (`## Position closed\n` +
91
+ `- Sold ${fill.qty} ${symbol} @ ${formatUsd(fill.priceUsd)} ` +
92
+ `(fee ${formatUsd(fill.feeUsd)})\n` +
93
+ `- Realized on this trade: ${formatUsd(tradeRealized)}\n` +
94
+ `- Remaining ${symbol}: ${remaining ? `${remaining.qty} @ avg ${formatUsd(remaining.avgPriceUsd)}` : '(flat)'}\n` +
95
+ `- Cash: ${formatUsd(portfolio.cashUsd)} · ` +
96
+ `Session realized P&L: ${formatUsd(portfolio.realizedPnlUsd)}`);
97
+ }
98
+ export function renderTradeHistory(params) {
99
+ const { windowRaw, entries, realized } = params;
100
+ const opens = entries.filter(e => e.side === 'buy').length;
101
+ const closes = entries.filter(e => e.side === 'sell').length;
102
+ const lines = [];
103
+ lines.push(`## Trade history (${windowRaw})`);
104
+ lines.push(`- ${windowRaw} P&L (realized): ${formatUsd(realized)}`);
105
+ lines.push(`- Trades: ${entries.length} (${opens} opens, ${closes} closes)`);
106
+ lines.push('');
107
+ if (entries.length === 0) {
108
+ lines.push('_No trades in this window._');
109
+ }
110
+ else {
111
+ for (const e of entries)
112
+ lines.push(formatTradeLine(e));
113
+ }
114
+ return lines.join('\n');
115
+ }
@@ -1,4 +1,7 @@
1
- import { getPrice, getOHLCV, getTrending, getMarketOverview } from '../trading/data.js';
1
+ import { getPrice, getOHLCV, getTrending, getMarketOverview, getFxPrice, getCommodityPrice, getStockPrice, } from '../trading/data.js';
2
+ const SUPPORTED_STOCK_MARKETS = [
3
+ 'us', 'hk', 'jp', 'kr', 'gb', 'de', 'fr', 'nl', 'ie', 'lu', 'cn', 'ca',
4
+ ];
2
5
  import { rsi, macd, bollingerBands, volatility } from '../trading/metrics.js';
3
6
  import { bus } from '../events/bus.js';
4
7
  import { makeEvent } from '../events/types.js';
@@ -102,8 +105,17 @@ export const tradingSignalCapability = {
102
105
  execute: executeSignal,
103
106
  concurrent: true,
104
107
  };
108
+ function formatPriceLine(label, priceUsd, change24hPct, opts = {}) {
109
+ const digits = opts.fractionDigits ?? 2;
110
+ const priceStr = `$${priceUsd.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits })}`;
111
+ if (opts.showChange === false || !Number.isFinite(change24hPct)) {
112
+ return `${label}: ${priceStr}`;
113
+ }
114
+ const sign = change24hPct > 0 ? '+' : '';
115
+ return `${label}: ${priceStr} (${sign}${change24hPct.toFixed(2)}% 24h)`;
116
+ }
105
117
  async function executeMarket(input, _ctx) {
106
- const { action, ticker } = input;
118
+ const { action, ticker, market } = input;
107
119
  if (!action) {
108
120
  return { output: 'Error: action is required', isError: true };
109
121
  }
@@ -121,6 +133,58 @@ async function executeMarket(input, _ctx) {
121
133
  output: `${ticker.toUpperCase()}: $${price.toLocaleString()} (${change24h > 0 ? '+' : ''}${change24h.toFixed(2)}% 24h), Market Cap: ${formatUsd(marketCap)}, Volume: ${formatUsd(volume24h)}`,
122
134
  };
123
135
  }
136
+ case 'fxPrice': {
137
+ if (!ticker) {
138
+ return { output: 'Error: ticker is required (e.g. "EUR-USD")', isError: true };
139
+ }
140
+ const result = await getFxPrice(ticker);
141
+ if (typeof result === 'string') {
142
+ return { output: `Error: ${result}`, isError: true };
143
+ }
144
+ return {
145
+ output: formatPriceLine(ticker.toUpperCase(), result.price, result.change24h, { fractionDigits: 4 }) +
146
+ ' · source: BlockRun Gateway / Pyth (free)',
147
+ };
148
+ }
149
+ case 'commodityPrice': {
150
+ if (!ticker) {
151
+ return { output: 'Error: ticker is required (e.g. "XAU-USD" for gold)', isError: true };
152
+ }
153
+ const result = await getCommodityPrice(ticker);
154
+ if (typeof result === 'string') {
155
+ return { output: `Error: ${result}`, isError: true };
156
+ }
157
+ return {
158
+ output: formatPriceLine(ticker.toUpperCase(), result.price, result.change24h, { fractionDigits: 2 }) +
159
+ ' · source: BlockRun Gateway / Pyth (free)',
160
+ };
161
+ }
162
+ case 'stockPrice': {
163
+ if (!ticker) {
164
+ return { output: 'Error: ticker is required (e.g. "AAPL" on market "us")', isError: true };
165
+ }
166
+ if (!market) {
167
+ return {
168
+ output: `Error: market code is required for stockPrice. Supported: ${SUPPORTED_STOCK_MARKETS.join(', ')}`,
169
+ isError: true,
170
+ };
171
+ }
172
+ if (!SUPPORTED_STOCK_MARKETS.includes(market)) {
173
+ return {
174
+ output: `Error: unsupported market "${market}". Supported: ${SUPPORTED_STOCK_MARKETS.join(', ')}`,
175
+ isError: true,
176
+ };
177
+ }
178
+ const result = await getStockPrice(ticker, market);
179
+ if (typeof result === 'string') {
180
+ return { output: `Error: ${result}`, isError: true };
181
+ }
182
+ const tickerLabel = `${ticker.toUpperCase()} (${market})`;
183
+ return {
184
+ output: formatPriceLine(tickerLabel, result.price, result.change24h, { fractionDigits: 2 }) +
185
+ ' · source: BlockRun Gateway / Pyth · $0.001 paid from wallet',
186
+ };
187
+ }
124
188
  case 'trending': {
125
189
  const result = await getTrending();
126
190
  if (typeof result === 'string') {
@@ -140,24 +204,39 @@ async function executeMarket(input, _ctx) {
140
204
  return { output: `Top 20 by Market Cap:\n${header}\n${sep}\n${rows.join('\n')}` };
141
205
  }
142
206
  default:
143
- return { output: `Error: unknown action "${action}". Use: price, trending, overview`, isError: true };
207
+ return {
208
+ output: `Error: unknown action "${action}". Use: price, trending, overview, fxPrice, commodityPrice, stockPrice`,
209
+ isError: true,
210
+ };
144
211
  }
145
212
  }
146
213
  export const tradingMarketCapability = {
147
214
  spec: {
148
215
  name: 'TradingMarket',
149
- description: 'Get cryptocurrency market data: price lookup, trending coins, or market overview (top 20 by market cap).',
216
+ description: 'Get market data across asset classes. Actions: ' +
217
+ '`price` (crypto spot via CoinGecko, free), ' +
218
+ '`trending` (top trending coins), ' +
219
+ '`overview` (top 20 by market cap), ' +
220
+ '`fxPrice` (FX pair like EUR-USD, BlockRun Gateway/Pyth, free), ' +
221
+ '`commodityPrice` (XAU-USD for gold, XAG-USD for silver, etc., free), ' +
222
+ '`stockPrice` (any of 1,746 tickers across us/hk/jp/kr/gb/de/fr/nl/ie/lu/cn/ca, BlockRun Gateway/Pyth, $0.001 per call paid from the agent wallet). ' +
223
+ 'Prefer stockPrice for any equity question — CRCL, AAPL, 7203.JP, 0005.HK, etc.',
150
224
  input_schema: {
151
225
  type: 'object',
152
226
  properties: {
153
227
  action: {
154
228
  type: 'string',
155
- enum: ['price', 'trending', 'overview'],
156
- description: 'What to fetch: price lookup, trending coins, or market overview',
229
+ enum: ['price', 'trending', 'overview', 'fxPrice', 'commodityPrice', 'stockPrice'],
230
+ description: 'What to fetch. See tool description for cost + source per action.',
157
231
  },
158
232
  ticker: {
159
233
  type: 'string',
160
- description: 'Cryptocurrency ticker (required for price action), e.g. "BTC"',
234
+ description: 'Ticker. Crypto: "BTC". FX: "EUR-USD". Commodity: "XAU-USD" (gold). Stock: "AAPL", "CRCL", "7203" (Toyota on jp), "0005" (HSBC on hk). Required for all price actions.',
235
+ },
236
+ market: {
237
+ type: 'string',
238
+ enum: ['us', 'hk', 'jp', 'kr', 'gb', 'de', 'fr', 'nl', 'ie', 'lu', 'cn', 'ca'],
239
+ description: 'Stock exchange market code. Required when action="stockPrice". Ignored for other actions.',
161
240
  },
162
241
  },
163
242
  required: ['action'],
@@ -0,0 +1,18 @@
1
+ /**
2
+ * WebhookPost — generic HTTP POST tool for pushing agent output to any
3
+ * webhook endpoint. Covers WeChat Work, Feishu, Discord, Slack, Telegram
4
+ * Bot API, PushPlus, ServerChan, custom HTTP receivers — anything that
5
+ * accepts a JSON body over POST.
6
+ *
7
+ * Intentionally not per-vendor: those integrations differ only in body
8
+ * shape, which the agent already knows how to construct. One tool, eight
9
+ * channels. If a channel needs a signature header (e.g., Feishu sign
10
+ * mode), the agent passes it in via `headers`.
11
+ *
12
+ * Safety: outbound URLs are a publish surface. We refuse localhost,
13
+ * private ranges, and file schemes so an agent can't be tricked into
14
+ * hitting internal services. A permission prompt fires on first use per
15
+ * session.
16
+ */
17
+ import type { CapabilityHandler } from '../agent/types.js';
18
+ export declare const webhookPostCapability: CapabilityHandler;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * WebhookPost — generic HTTP POST tool for pushing agent output to any
3
+ * webhook endpoint. Covers WeChat Work, Feishu, Discord, Slack, Telegram
4
+ * Bot API, PushPlus, ServerChan, custom HTTP receivers — anything that
5
+ * accepts a JSON body over POST.
6
+ *
7
+ * Intentionally not per-vendor: those integrations differ only in body
8
+ * shape, which the agent already knows how to construct. One tool, eight
9
+ * channels. If a channel needs a signature header (e.g., Feishu sign
10
+ * mode), the agent passes it in via `headers`.
11
+ *
12
+ * Safety: outbound URLs are a publish surface. We refuse localhost,
13
+ * private ranges, and file schemes so an agent can't be tricked into
14
+ * hitting internal services. A permission prompt fires on first use per
15
+ * session.
16
+ */
17
+ import { isIP } from 'node:net';
18
+ const DEFAULT_TIMEOUT_MS = 15_000;
19
+ const MAX_BODY_BYTES = 512 * 1024; // 512 KB is generous for a chat push.
20
+ function isPrivateHost(hostname) {
21
+ const h = hostname
22
+ .trim()
23
+ .replace(/^\[/, '')
24
+ .replace(/\]$/, '')
25
+ .split('%', 1)[0]
26
+ .toLowerCase();
27
+ if (h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '::' || h === '::1')
28
+ return true;
29
+ // IPv4 private ranges.
30
+ if (isIP(h) === 4) {
31
+ const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(h);
32
+ if (m) {
33
+ const [a, b] = [Number(m[1]), Number(m[2])];
34
+ if (a === 10)
35
+ return true;
36
+ if (a === 172 && b >= 16 && b <= 31)
37
+ return true;
38
+ if (a === 192 && b === 168)
39
+ return true;
40
+ if (a === 169 && b === 254)
41
+ return true; // link-local
42
+ if (a === 127)
43
+ return true;
44
+ }
45
+ }
46
+ if (isIP(h) === 6) {
47
+ if (h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe80:'))
48
+ return true;
49
+ if (h.startsWith('::ffff:')) {
50
+ return isPrivateHost(h.slice('::ffff:'.length));
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+ async function execute(input, ctx) {
56
+ const { url, body, headers, method = 'POST' } = input;
57
+ if (!url || typeof url !== 'string') {
58
+ return { output: 'Error: url is required (string).', isError: true };
59
+ }
60
+ let parsed;
61
+ try {
62
+ parsed = new URL(url);
63
+ }
64
+ catch {
65
+ return { output: `Error: invalid URL: ${url}`, isError: true };
66
+ }
67
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
68
+ return { output: `Error: only http(s) URLs allowed, got ${parsed.protocol}`, isError: true };
69
+ }
70
+ if (isPrivateHost(parsed.hostname)) {
71
+ return {
72
+ output: `Error: refusing to post to private/loopback host ${parsed.hostname}. ` +
73
+ `WebhookPost is for public webhook endpoints only.`,
74
+ isError: true,
75
+ };
76
+ }
77
+ // Serialize body. Accept object/array (JSON.stringify) or string (used as-is).
78
+ let bodyText;
79
+ let contentType = 'application/json';
80
+ if (typeof body === 'string') {
81
+ bodyText = body;
82
+ contentType = 'text/plain';
83
+ }
84
+ else if (body === undefined || body === null) {
85
+ bodyText = '';
86
+ }
87
+ else {
88
+ try {
89
+ bodyText = JSON.stringify(body);
90
+ }
91
+ catch (err) {
92
+ return { output: `Error: body is not JSON-serializable: ${err.message}`, isError: true };
93
+ }
94
+ }
95
+ const bodyBytes = Buffer.byteLength(bodyText, 'utf-8');
96
+ if (bodyBytes > MAX_BODY_BYTES) {
97
+ return {
98
+ output: `Error: body is ${(bodyBytes / 1024).toFixed(1)} KB, exceeds ${MAX_BODY_BYTES / 1024} KB cap.`,
99
+ isError: true,
100
+ };
101
+ }
102
+ const finalHeaders = {
103
+ 'Content-Type': contentType,
104
+ 'User-Agent': 'franklin/3.8.9 (webhook)',
105
+ ...(headers ?? {}),
106
+ };
107
+ const ctrl = new AbortController();
108
+ // Chain abort from the execution scope so Ctrl+C cancels the webhook call.
109
+ const onParentAbort = () => ctrl.abort();
110
+ ctx.abortSignal.addEventListener('abort', onParentAbort);
111
+ const timer = setTimeout(() => ctrl.abort(), DEFAULT_TIMEOUT_MS);
112
+ try {
113
+ const res = await fetch(url, {
114
+ method,
115
+ headers: finalHeaders,
116
+ body: bodyText || undefined,
117
+ signal: ctrl.signal,
118
+ });
119
+ // Capture a small slice of the response body for debugging — most webhook
120
+ // endpoints return a short ack ({"ok":true}, "ok", "1", etc.).
121
+ const text = await res.text();
122
+ const preview = text.length > 500 ? text.slice(0, 500) + '...' : text;
123
+ if (!res.ok) {
124
+ return {
125
+ output: `Webhook POST failed: HTTP ${res.status} ${res.statusText}\nResponse: ${preview}`,
126
+ isError: true,
127
+ };
128
+ }
129
+ return {
130
+ output: `Posted ${bodyBytes}B to ${parsed.host}${parsed.pathname}\n` +
131
+ `Response ${res.status}: ${preview || '(empty)'}`,
132
+ };
133
+ }
134
+ catch (err) {
135
+ if (err.name === 'AbortError') {
136
+ if (ctx.abortSignal.aborted)
137
+ return { output: 'Webhook POST canceled by user.', isError: true };
138
+ return { output: `Webhook POST timed out after ${DEFAULT_TIMEOUT_MS}ms`, isError: true };
139
+ }
140
+ return { output: `Webhook POST error: ${err.message}`, isError: true };
141
+ }
142
+ finally {
143
+ clearTimeout(timer);
144
+ ctx.abortSignal.removeEventListener('abort', onParentAbort);
145
+ }
146
+ }
147
+ export const webhookPostCapability = {
148
+ spec: {
149
+ name: 'WebhookPost',
150
+ description: 'POST a JSON or plain-text payload to a webhook URL. Works with any service that ' +
151
+ 'accepts an HTTP POST: WeChat Work bots, Feishu/Lark bots, Discord/Slack webhooks, ' +
152
+ 'Telegram Bot API (sendMessage), PushPlus, ServerChan, or a custom receiver. ' +
153
+ 'The agent is responsible for the body shape — each channel has its own schema ' +
154
+ '(e.g., Discord: { "content": "hello" }; WeChat Work: { "msgtype": "markdown", ' +
155
+ '"markdown": { "content": "..." } }).\n\n' +
156
+ 'Safety: private/loopback hosts are refused. Bodies over 512KB are refused. ' +
157
+ 'Do NOT use for GET requests — use WebFetch for reads.',
158
+ input_schema: {
159
+ type: 'object',
160
+ required: ['url', 'body'],
161
+ properties: {
162
+ url: {
163
+ type: 'string',
164
+ description: 'Full https (or http) webhook URL. Must be a public host.',
165
+ },
166
+ body: {
167
+ description: 'Request body. Pass an object/array → JSON.stringify. Pass a string → sent ' +
168
+ 'as text/plain. Construct the shape each channel expects.',
169
+ },
170
+ headers: {
171
+ type: 'object',
172
+ description: 'Optional extra request headers (auth tokens, signing headers). Content-Type ' +
173
+ 'is set automatically based on body type.',
174
+ },
175
+ method: {
176
+ type: 'string',
177
+ enum: ['POST', 'PUT', 'PATCH'],
178
+ description: 'HTTP method. Defaults to POST.',
179
+ },
180
+ },
181
+ },
182
+ },
183
+ execute,
184
+ concurrent: false,
185
+ };
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Legacy data.ts — thin shim over the provider registry.
3
+ *
4
+ * Keeps the historical `string | Data` error-by-string return convention so
5
+ * the existing callers (`trading.ts`, `tools/index.ts` wiring into
6
+ * `LiveExchange`) keep working without a call-site rewrite. New code should
7
+ * import from `./providers/registry.js` and consume `PriceData | ProviderError`
8
+ * directly — the structured error shape enables UI color-coding and retry
9
+ * classification the string convention can't express.
10
+ */
11
+ import type { AssetClass, MarketCode } from './providers/standard-models.js';
1
12
  export interface PriceData {
2
13
  price: number;
3
14
  change24h: number;
@@ -24,7 +35,19 @@ export interface MarketCoin {
24
35
  volume24h: number;
25
36
  }
26
37
  export declare function resolveId(ticker: string): string;
27
- export declare function getPrice(ticker: string): Promise<PriceData | string>;
38
+ /**
39
+ * Look up a spot price. `assetClass` defaults to 'crypto' so all existing
40
+ * crypto-only callers (`TradingSignal`, `LiveExchange.getPrice`) behave
41
+ * exactly as before. Pass 'fx' / 'commodity' / 'stock' (plus a `market`
42
+ * code for stocks) to hit the multi-asset Gateway endpoints.
43
+ */
44
+ export declare function getPrice(ticker: string, assetClass?: AssetClass, market?: MarketCode): Promise<PriceData | string>;
45
+ /** Convenience: FX pair lookup (e.g. "EUR-USD"). */
46
+ export declare function getFxPrice(ticker: string): Promise<PriceData | string>;
47
+ /** Convenience: commodity lookup (e.g. "XAU-USD" for gold). */
48
+ export declare function getCommodityPrice(ticker: string): Promise<PriceData | string>;
49
+ /** Convenience: stock lookup (e.g. "AAPL" on market "us"). */
50
+ export declare function getStockPrice(ticker: string, market: MarketCode): Promise<PriceData | string>;
28
51
  export declare function getOHLCV(ticker: string, days?: number): Promise<OHLCVData | string>;
29
52
  export declare function getTrending(): Promise<TrendingCoin[] | string>;
30
53
  export declare function getMarketOverview(): Promise<MarketCoin[] | string>;