@blockrun/franklin 3.2.4 → 3.3.1
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/README.md +216 -233
- package/dist/agent/commands.js +54 -13
- package/dist/agent/context.js +31 -1
- package/dist/agent/loop.js +48 -19
- package/dist/agent/permissions.js +3 -3
- package/dist/commands/migrate.d.ts +13 -0
- package/dist/commands/migrate.js +389 -0
- package/dist/commands/panel.d.ts +6 -0
- package/dist/commands/panel.js +29 -0
- package/dist/commands/start.js +41 -2
- package/dist/events/bridge.d.ts +1 -0
- package/dist/events/bridge.js +24 -0
- package/dist/events/bus.d.ts +17 -0
- package/dist/events/bus.js +55 -0
- package/dist/events/types.d.ts +49 -0
- package/dist/events/types.js +8 -0
- package/dist/index.js +15 -0
- package/dist/learnings/extractor.d.ts +16 -0
- package/dist/learnings/extractor.js +234 -0
- package/dist/learnings/index.d.ts +3 -0
- package/dist/learnings/index.js +2 -0
- package/dist/learnings/store.d.ts +15 -0
- package/dist/learnings/store.js +130 -0
- package/dist/learnings/types.d.ts +24 -0
- package/dist/learnings/types.js +7 -0
- package/dist/mcp/client.js +9 -2
- package/dist/narrative/state.d.ts +30 -0
- package/dist/narrative/state.js +69 -0
- package/dist/panel/html.d.ts +5 -0
- package/dist/panel/html.js +341 -0
- package/dist/panel/server.d.ts +7 -0
- package/dist/panel/server.js +152 -0
- package/dist/session/storage.js +4 -2
- package/dist/social/browser-pool.d.ts +29 -0
- package/dist/social/browser-pool.js +57 -0
- package/dist/social/preflight.d.ts +14 -0
- package/dist/social/preflight.js +26 -0
- package/dist/social/x.d.ts +8 -0
- package/dist/social/x.js +9 -1
- package/dist/stats/tracker.d.ts +1 -0
- package/dist/stats/tracker.js +59 -13
- package/dist/tools/bash.js +6 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/posttox.d.ts +7 -0
- package/dist/tools/posttox.js +137 -0
- package/dist/tools/searchx.d.ts +7 -0
- package/dist/tools/searchx.js +111 -0
- package/dist/tools/trading.d.ts +3 -0
- package/dist/tools/trading.js +168 -0
- package/dist/tools/webfetch.js +19 -9
- package/dist/tools/write.js +2 -0
- package/dist/trading/config.d.ts +23 -0
- package/dist/trading/config.js +45 -0
- package/dist/trading/data.d.ts +30 -0
- package/dist/trading/data.js +112 -0
- package/dist/trading/metrics.d.ts +29 -0
- package/dist/trading/metrics.js +105 -0
- package/dist/ui/app.js +73 -44
- package/dist/ui/markdown.d.ts +9 -0
- package/dist/ui/markdown.js +86 -0
- 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).*');
|
package/dist/stats/tracker.d.ts
CHANGED
package/dist/stats/tracker.js
CHANGED
|
@@ -6,7 +6,47 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import { OPUS_PRICING } from '../pricing.js';
|
|
9
|
-
|
|
9
|
+
import { BLOCKRUN_DIR } from '../config.js';
|
|
10
|
+
let resolvedStatsFile = null;
|
|
11
|
+
function preferredStatsFile() {
|
|
12
|
+
return path.join(BLOCKRUN_DIR, 'runcode-stats.json');
|
|
13
|
+
}
|
|
14
|
+
function fallbackStatsFile() {
|
|
15
|
+
return path.join(os.tmpdir(), 'runcode', 'runcode-stats.json');
|
|
16
|
+
}
|
|
17
|
+
export function getStatsFilePath() {
|
|
18
|
+
if (resolvedStatsFile)
|
|
19
|
+
return resolvedStatsFile;
|
|
20
|
+
for (const file of [preferredStatsFile(), fallbackStatsFile()]) {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
23
|
+
resolvedStatsFile = file;
|
|
24
|
+
return file;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Try the next candidate.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
resolvedStatsFile = preferredStatsFile();
|
|
31
|
+
return resolvedStatsFile;
|
|
32
|
+
}
|
|
33
|
+
function withWritableStatsFile(action) {
|
|
34
|
+
const preferred = preferredStatsFile();
|
|
35
|
+
const fallback = fallbackStatsFile();
|
|
36
|
+
try {
|
|
37
|
+
action(getStatsFilePath());
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const code = err.code;
|
|
41
|
+
const shouldFallback = (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') &&
|
|
42
|
+
resolvedStatsFile === preferred;
|
|
43
|
+
if (!shouldFallback)
|
|
44
|
+
throw err;
|
|
45
|
+
fs.mkdirSync(path.dirname(fallback), { recursive: true });
|
|
46
|
+
resolvedStatsFile = fallback;
|
|
47
|
+
action(fallback);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
10
50
|
const EMPTY_STATS = {
|
|
11
51
|
version: 1,
|
|
12
52
|
totalRequests: 0,
|
|
@@ -19,8 +59,9 @@ const EMPTY_STATS = {
|
|
|
19
59
|
};
|
|
20
60
|
export function loadStats() {
|
|
21
61
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
62
|
+
const statsFile = getStatsFilePath();
|
|
63
|
+
if (fs.existsSync(statsFile)) {
|
|
64
|
+
const data = JSON.parse(fs.readFileSync(statsFile, 'utf-8'));
|
|
24
65
|
// Migration: add missing fields
|
|
25
66
|
return {
|
|
26
67
|
...EMPTY_STATS,
|
|
@@ -36,10 +77,12 @@ export function loadStats() {
|
|
|
36
77
|
}
|
|
37
78
|
export function saveStats(stats) {
|
|
38
79
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
withWritableStatsFile((statsFile) => {
|
|
81
|
+
fs.mkdirSync(path.dirname(statsFile), { recursive: true });
|
|
82
|
+
// Keep only last 1000 history records
|
|
83
|
+
stats.history = stats.history.slice(-1000);
|
|
84
|
+
fs.writeFileSync(statsFile, JSON.stringify(stats, null, 2));
|
|
85
|
+
});
|
|
43
86
|
}
|
|
44
87
|
catch {
|
|
45
88
|
/* ignore write errors */
|
|
@@ -51,13 +94,16 @@ export function clearStats() {
|
|
|
51
94
|
clearTimeout(flushTimer);
|
|
52
95
|
flushTimer = null;
|
|
53
96
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
97
|
+
resolvedStatsFile = null;
|
|
98
|
+
for (const statsFile of new Set([preferredStatsFile(), fallbackStatsFile()])) {
|
|
99
|
+
try {
|
|
100
|
+
if (fs.existsSync(statsFile)) {
|
|
101
|
+
fs.unlinkSync(statsFile);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* ignore */
|
|
57
106
|
}
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
/* ignore */
|
|
61
107
|
}
|
|
62
108
|
}
|
|
63
109
|
// ─── In-memory stats cache with debounced write ─────────────────────────
|
package/dist/tools/bash.js
CHANGED
|
@@ -193,6 +193,7 @@ async function execute(input, ctx) {
|
|
|
193
193
|
let outputBytes = 0;
|
|
194
194
|
let truncated = false;
|
|
195
195
|
let killed = false;
|
|
196
|
+
let abortedByUser = false;
|
|
196
197
|
const timer = setTimeout(() => {
|
|
197
198
|
killed = true;
|
|
198
199
|
child.kill('SIGTERM');
|
|
@@ -206,6 +207,7 @@ async function execute(input, ctx) {
|
|
|
206
207
|
// Handle abort signal
|
|
207
208
|
const onAbort = () => {
|
|
208
209
|
killed = true;
|
|
210
|
+
abortedByUser = true;
|
|
209
211
|
child.kill('SIGTERM');
|
|
210
212
|
};
|
|
211
213
|
ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
@@ -293,8 +295,11 @@ async function execute(input, ctx) {
|
|
|
293
295
|
result = `... (${omitted.toLocaleString()} chars omitted from start)\n${trimmed}`;
|
|
294
296
|
}
|
|
295
297
|
if (killed) {
|
|
298
|
+
const reason = abortedByUser
|
|
299
|
+
? 'aborted by user'
|
|
300
|
+
: `timeout after ${timeoutMs / 1000}s. Set timeout param up to 600000ms for longer.`;
|
|
296
301
|
resolve({
|
|
297
|
-
output: result + `\n\n(command killed —
|
|
302
|
+
output: result + `\n\n(command killed — ${reason})`,
|
|
298
303
|
isError: true,
|
|
299
304
|
});
|
|
300
305
|
return;
|
package/dist/tools/index.js
CHANGED
|
@@ -12,6 +12,7 @@ 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';
|
|
15
16
|
/** All capabilities available to the runcode agent (excluding sub-agent, which needs config). */
|
|
16
17
|
export const allCapabilities = [
|
|
17
18
|
readCapability,
|
|
@@ -25,6 +26,8 @@ export const allCapabilities = [
|
|
|
25
26
|
taskCapability,
|
|
26
27
|
imageGenCapability,
|
|
27
28
|
askUserCapability,
|
|
29
|
+
tradingSignalCapability,
|
|
30
|
+
tradingMarketCapability,
|
|
28
31
|
];
|
|
29
32
|
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
30
33
|
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,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
|
+
};
|