@aiaiaichain/agent 0.1.4 → 0.1.5
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/cli.js +218 -5
- package/dist/core/ChainConfig.js +1 -1
- package/dist/core/SystemMonitor.d.ts +1 -4
- package/dist/core/SystemMonitor.js +20 -46
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/models/ModelRegistry.js +12 -4
- package/dist/runner/AgentRunner.d.ts +2 -0
- package/dist/runner/AgentRunner.js +18 -1
- package/dist/runner/ModelClient.js +109 -48
- package/dist/session/SessionManager.d.ts +1 -0
- package/dist/session/SessionManager.js +8 -2
- package/dist/session/SessionStore.js +8 -3
- package/dist/tools/CrossTools.js +13 -5
- package/dist/tools/MarketSentiment.js +22 -13
- package/dist/tools/NewsSentiment.js +9 -3
- package/dist/tools/PriceFeed.js +11 -4
- package/dist/tools/TechnicalAnalysis.js +2 -1
- package/dist/tools/TokenCalendar.d.ts +24 -0
- package/dist/tools/TokenCalendar.js +81 -0
- package/dist/tools/TokenSecurityScanner.d.ts +22 -0
- package/dist/tools/TokenSecurityScanner.js +102 -0
- package/dist/tools/TransactionSim.d.ts +17 -0
- package/dist/tools/TransactionSim.js +78 -0
- package/dist/tui/App.js +143 -21
- package/dist/tui/REPL.js +2 -2
- package/dist/tui/Sparkline.d.ts +21 -0
- package/dist/tui/Sparkline.js +44 -0
- package/dist/tui/ThemePresets.d.ts +25 -0
- package/dist/tui/ThemePresets.js +117 -0
- package/dist/util/clipboard.d.ts +9 -0
- package/dist/util/clipboard.js +26 -0
- package/dist/util/commandSuggest.d.ts +7 -0
- package/dist/util/commandSuggest.js +44 -0
- package/dist/util/confirmation.d.ts +6 -0
- package/dist/util/confirmation.js +16 -0
- package/dist/util/errorHandler.d.ts +3 -0
- package/dist/util/errorHandler.js +28 -0
- package/dist/util/logger.d.ts +11 -0
- package/dist/util/logger.js +43 -0
- package/dist/util/processManager.d.ts +5 -0
- package/dist/util/processManager.js +39 -0
- package/dist/util/resilientFetch.d.ts +21 -0
- package/dist/util/resilientFetch.js +94 -0
- package/dist/util/responseCache.d.ts +27 -0
- package/dist/util/responseCache.js +54 -0
- package/dist/util/safeLog.d.ts +4 -5
- package/dist/util/safeLog.js +68 -30
- package/dist/util/scheduler.d.ts +14 -0
- package/dist/util/scheduler.js +75 -0
- package/dist/util/webhooks.d.ts +9 -0
- package/dist/util/webhooks.js +75 -0
- package/dist/wallet/ActionFeed.js +12 -4
- package/dist/wallet/AgentWallet.d.ts +1 -0
- package/dist/wallet/AgentWallet.js +30 -5
- package/dist/wallet/ProfitTracker.d.ts +30 -0
- package/dist/wallet/ProfitTracker.js +93 -0
- package/package.json +1 -1
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
* Handles streaming and non-streaming calls.
|
|
4
4
|
*/
|
|
5
5
|
import { env } from "../core/EnvLoader.js";
|
|
6
|
+
import { logger } from "../util/logger.js";
|
|
7
|
+
import { resilientFetch } from "../util/resilientFetch.js";
|
|
8
|
+
const MAX_RETRIES = 2;
|
|
9
|
+
const RETRY_BACKOFF_MS = 1000;
|
|
10
|
+
const REQUEST_TIMEOUT = 60_000;
|
|
11
|
+
function isRetryable(status) {
|
|
12
|
+
return status === 429 || status >= 500;
|
|
13
|
+
}
|
|
6
14
|
export function resolveModelConfig(registry) {
|
|
7
15
|
const apiKey = env.get("OPENROUTER_API_KEY");
|
|
8
16
|
const defaultModel = env.get("DEFAULT_MODEL");
|
|
@@ -24,61 +32,114 @@ export class ModelClient {
|
|
|
24
32
|
stream: true,
|
|
25
33
|
max_tokens: 4096,
|
|
26
34
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
35
|
+
let lastError = "";
|
|
36
|
+
let lastStatus = 0;
|
|
37
|
+
let timer;
|
|
38
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
if (attempt > 0) {
|
|
41
|
+
const delay = RETRY_BACKOFF_MS * Math.pow(2, attempt - 1);
|
|
42
|
+
logger.info('ModelClient', `Retry attempt ${attempt}/${MAX_RETRIES} after ${delay}ms`);
|
|
43
|
+
await new Promise(r => setTimeout(r, delay));
|
|
44
|
+
}
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
47
|
+
if (signal?.aborted) {
|
|
48
|
+
onEvent({ type: "error", message: "Request aborted" });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const combinedSignal = signal ?? controller.signal;
|
|
52
|
+
const response = await resilientFetch(`${config.baseUrl}/chat/completions`, {
|
|
53
|
+
timeout: REQUEST_TIMEOUT,
|
|
54
|
+
retries: 0,
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
59
|
+
"HTTP-Referer": "https://aiaiaichain.dev",
|
|
60
|
+
"X-Title": "AIAIAI Chain Agent",
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
signal: combinedSignal,
|
|
64
|
+
});
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const err = await response.text().catch(() => "unknown error");
|
|
68
|
+
lastError = err;
|
|
69
|
+
lastStatus = response.status;
|
|
70
|
+
if (isRetryable(response.status) && attempt < MAX_RETRIES) {
|
|
71
|
+
logger.warn('ModelClient', `Retryable error ${response.status}`, { attempt });
|
|
63
72
|
continue;
|
|
64
|
-
try {
|
|
65
|
-
const json = JSON.parse(trimmed.slice(6));
|
|
66
|
-
const delta = json.choices?.[0]?.delta?.content;
|
|
67
|
-
if (delta)
|
|
68
|
-
onEvent({ type: "text_delta", text: delta });
|
|
69
73
|
}
|
|
70
|
-
|
|
74
|
+
if (response.status === 401) {
|
|
75
|
+
onEvent({ type: "error", message: `API auth failed: OpenRouter key is missing or invalid.\nRun: aiaiai setup\nGet a key: https://openrouter.ai/keys` });
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
onEvent({ type: "error", message: `API error ${response.status}: ${err}` });
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const reader = response.body?.getReader();
|
|
83
|
+
if (!reader) {
|
|
84
|
+
onEvent({ type: "error", message: "No response body" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const decoder = new TextDecoder();
|
|
88
|
+
let buffer = "";
|
|
89
|
+
while (true) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done)
|
|
92
|
+
break;
|
|
93
|
+
buffer += decoder.decode(value, { stream: true });
|
|
94
|
+
const lines = buffer.split("\n");
|
|
95
|
+
buffer = lines.pop() || "";
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed || trimmed === "data: [DONE]")
|
|
99
|
+
continue;
|
|
100
|
+
if (!trimmed.startsWith("data: "))
|
|
101
|
+
continue;
|
|
102
|
+
try {
|
|
103
|
+
const json = JSON.parse(trimmed.slice(6));
|
|
104
|
+
const delta = json.choices?.[0]?.delta?.content;
|
|
105
|
+
if (delta)
|
|
106
|
+
onEvent({ type: "text_delta", text: delta });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
logger.debug('ModelClient', 'Skipped malformed SSE line', { line: trimmed.slice(0, 100) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
71
112
|
}
|
|
72
|
-
}
|
|
73
|
-
onEvent({ type: "turn_done" });
|
|
74
|
-
}
|
|
75
|
-
catch (e) {
|
|
76
|
-
if (e instanceof Error && e.name === "AbortError") {
|
|
77
113
|
onEvent({ type: "turn_done" });
|
|
114
|
+
return; // Success — exit retry loop
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
if (e instanceof Error && e.name === "AbortError") {
|
|
119
|
+
if (signal?.aborted) {
|
|
120
|
+
onEvent({ type: "turn_done" });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Timeout
|
|
124
|
+
lastError = "Request timed out";
|
|
125
|
+
if (attempt < MAX_RETRIES) {
|
|
126
|
+
logger.warn('ModelClient', `Timeout, retrying`, { attempt });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
onEvent({ type: "error", message: "Request timed out" });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
133
|
+
if (attempt < MAX_RETRIES) {
|
|
134
|
+
logger.warn('ModelClient', `Error, retrying`, { attempt, error: lastError });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
onEvent({ type: "error", message: `ModelClient error (${lastStatus || 'offline'}): ${lastError}` });
|
|
78
138
|
return;
|
|
79
139
|
}
|
|
80
|
-
onEvent({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
81
140
|
}
|
|
141
|
+
// Exhausted retries
|
|
142
|
+
onEvent({ type: "error", message: `ModelClient failed after ${MAX_RETRIES + 1} attempts: ${lastError}` });
|
|
82
143
|
}
|
|
83
144
|
}
|
|
84
145
|
//# sourceMappingURL=ModelClient.js.map
|
|
@@ -5,6 +5,7 @@ export class SessionManager {
|
|
|
5
5
|
messages = [];
|
|
6
6
|
systemPrompt = "";
|
|
7
7
|
maxContextTokens = 128_000;
|
|
8
|
+
_cachedChars = 0;
|
|
8
9
|
setSystemPrompt(prompt) {
|
|
9
10
|
this.systemPrompt = prompt;
|
|
10
11
|
}
|
|
@@ -13,13 +14,18 @@ export class SessionManager {
|
|
|
13
14
|
}
|
|
14
15
|
addMessage(role, content) {
|
|
15
16
|
this.messages.push({ role, content });
|
|
17
|
+
this._cachedChars += content.length + role.length + 10;
|
|
16
18
|
}
|
|
17
19
|
getMessages() {
|
|
18
20
|
// Estimate: 1 token ≈ 4 chars
|
|
19
21
|
const maxChars = this.maxContextTokens * 4;
|
|
22
|
+
// Fast path: if total is under limit, return all
|
|
23
|
+
if (this._cachedChars <= maxChars) {
|
|
24
|
+
return [...this.messages];
|
|
25
|
+
}
|
|
26
|
+
// Slow path: walk backwards until we hit the limit
|
|
20
27
|
let total = 0;
|
|
21
28
|
const result = [];
|
|
22
|
-
// Walk backwards, building up until we hit the limit
|
|
23
29
|
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
24
30
|
const msg = this.messages[i];
|
|
25
31
|
const size = msg.content.length + msg.role.length + 10;
|
|
@@ -31,7 +37,7 @@ export class SessionManager {
|
|
|
31
37
|
return result;
|
|
32
38
|
}
|
|
33
39
|
getContextPressure() {
|
|
34
|
-
const totalChars = this.messages.reduce((sum, m) => sum + m.content.length + m.role.length + 10, 0);
|
|
40
|
+
const totalChars = this._cachedChars || this.messages.reduce((sum, m) => sum + m.content.length + m.role.length + 10, 0);
|
|
35
41
|
const maxChars = this.maxContextTokens * 4;
|
|
36
42
|
const pct = Math.min(100, Math.round((totalChars / maxChars) * 100));
|
|
37
43
|
let level = "green";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
+
import { logger } from '../util/logger.js';
|
|
8
9
|
const SESSIONS_DIR = resolve(homedir(), '.aiaiai', 'sessions');
|
|
9
10
|
function ensureDir() {
|
|
10
11
|
if (!existsSync(SESSIONS_DIR))
|
|
@@ -63,7 +64,8 @@ export class SessionStore {
|
|
|
63
64
|
return null;
|
|
64
65
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
65
66
|
}
|
|
66
|
-
catch {
|
|
67
|
+
catch (error) {
|
|
68
|
+
logger.debug('SessionStore', 'Failed to load session', { error: error.message });
|
|
67
69
|
return null;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -85,11 +87,14 @@ export class SessionStore {
|
|
|
85
87
|
model: data.model,
|
|
86
88
|
});
|
|
87
89
|
}
|
|
88
|
-
catch {
|
|
90
|
+
catch (error) {
|
|
91
|
+
logger.debug('SessionStore', 'Skipped bad session file', { error: error.message });
|
|
92
|
+
}
|
|
89
93
|
}
|
|
90
94
|
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
91
95
|
}
|
|
92
|
-
catch {
|
|
96
|
+
catch (error) {
|
|
97
|
+
logger.warn('SessionStore', 'Failed to list sessions', { error: error.message });
|
|
93
98
|
return [];
|
|
94
99
|
}
|
|
95
100
|
}
|
package/dist/tools/CrossTools.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
6
|
import { priceFeed } from "../tools/PriceFeed.js";
|
|
7
7
|
import { AIAIAI_TOKEN, agentWallet } from "../wallet/AgentWallet.js";
|
|
8
|
+
import { logger } from "../util/logger.js";
|
|
8
9
|
const alerts = [];
|
|
9
10
|
export function addAlert(token, type, price) {
|
|
10
11
|
const alert = {
|
|
@@ -111,7 +112,8 @@ export async function compareTokensTool(_id, params) {
|
|
|
111
112
|
price1 = await priceFeed.fetchToken(addr1);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
|
-
catch {
|
|
115
|
+
catch (error) {
|
|
116
|
+
logger.warn('CrossTools', 'compareTokens: failed to fetch price1', { address: addr1, error: error.message });
|
|
115
117
|
price1 = null;
|
|
116
118
|
}
|
|
117
119
|
try {
|
|
@@ -122,7 +124,8 @@ export async function compareTokensTool(_id, params) {
|
|
|
122
124
|
price2 = await priceFeed.fetchToken(addr2);
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
|
-
catch {
|
|
127
|
+
catch (error) {
|
|
128
|
+
logger.warn('CrossTools', 'compareTokens: failed to fetch price2', { address: addr2, error: error.message });
|
|
126
129
|
price2 = null;
|
|
127
130
|
}
|
|
128
131
|
const lines = [
|
|
@@ -159,16 +162,21 @@ export async function portfolioTool(_id, params) {
|
|
|
159
162
|
else if (address === DEPOSIT_WALLET)
|
|
160
163
|
result = all.deposit;
|
|
161
164
|
}
|
|
162
|
-
catch {
|
|
165
|
+
catch (error) {
|
|
166
|
+
logger.warn('CrossTools', 'portfolioTool: wallet lookup failed', { address, error: error.message });
|
|
167
|
+
}
|
|
163
168
|
if (result) {
|
|
169
|
+
let solPrice = await new Promise(res => {
|
|
170
|
+
agentWallet.getSolPrice().then(res).catch(() => res(150));
|
|
171
|
+
});
|
|
164
172
|
const lines = [
|
|
165
173
|
`📊 Portfolio: ${address.slice(0, 8)}…${address.slice(-6)}`,
|
|
166
174
|
'',
|
|
167
|
-
` SOL: ${result.sol.toFixed(4)} ($${(result.sol *
|
|
175
|
+
` SOL: ${result.sol.toFixed(4)} ($${(result.sol * solPrice).toFixed(2)} est)`,
|
|
168
176
|
` $AIAIAI: ${result.aiaiai.toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
|
|
169
177
|
` USDC: ${result.usdc.toFixed(2)}`,
|
|
170
178
|
'',
|
|
171
|
-
` Total value (est): $${(result.sol *
|
|
179
|
+
` Total value (est): $${(result.sol * solPrice + result.usdc).toFixed(2)} + $AIAIAI`,
|
|
172
180
|
];
|
|
173
181
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
174
182
|
}
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* Fear & Greed Index, Binance Funding Rates, BTC Mempool, DeFi TVL, Solana Stats.
|
|
4
4
|
*/
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { resilientFetch } from "../util/resilientFetch.js";
|
|
7
|
+
import { logger } from "../util/logger.js";
|
|
6
8
|
// ── Fear & Greed Index ──────────────────────────────────────────────────────
|
|
7
9
|
export const fearGreedParams = Type.Object({});
|
|
8
10
|
export async function getFearGreedTool() {
|
|
9
11
|
try {
|
|
10
|
-
const response = await
|
|
12
|
+
const response = await resilientFetch("https://api.alternative.me/fng/?limit=7", { timeout: 10_000, retries: 1 });
|
|
11
13
|
if (!response.ok)
|
|
12
14
|
return fallbackFearGreed();
|
|
13
15
|
const data = await response.json();
|
|
@@ -28,7 +30,8 @@ export async function getFearGreedTool() {
|
|
|
28
30
|
}],
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
|
-
catch {
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger.warn('MarketSentiment', 'Fear & Greed fetch failed', { error: error.message });
|
|
32
35
|
return fallbackFearGreed();
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -47,7 +50,7 @@ export const fundingRatesParams = Type.Object({
|
|
|
47
50
|
export async function getFundingRatesTool(_id, params) {
|
|
48
51
|
const limit = params.limit || 10;
|
|
49
52
|
try {
|
|
50
|
-
const response = await
|
|
53
|
+
const response = await resilientFetch("https://fapi.binance.com/fapi/v1/premiumIndex", { timeout: 10_000, retries: 1 });
|
|
51
54
|
if (!response.ok)
|
|
52
55
|
return fallbackFundingRates();
|
|
53
56
|
const data = await response.json();
|
|
@@ -67,7 +70,8 @@ export async function getFundingRatesTool(_id, params) {
|
|
|
67
70
|
}],
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
|
-
catch {
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.warn('MarketSentiment', 'Funding rates fetch failed', { error: error.message });
|
|
71
75
|
return fallbackFundingRates();
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -84,8 +88,8 @@ export const btcMempoolParams = Type.Object({});
|
|
|
84
88
|
export async function getBtcMempoolTool() {
|
|
85
89
|
try {
|
|
86
90
|
const [feeResp, mempoolResp] = await Promise.all([
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
resilientFetch("https://mempool.space/api/v1/fees/recommended", { timeout: 10_000 }),
|
|
92
|
+
resilientFetch("https://mempool.space/api/mempool", { timeout: 10_000 }),
|
|
89
93
|
]);
|
|
90
94
|
const fees = await feeResp.json();
|
|
91
95
|
const mempool = await mempoolResp.json();
|
|
@@ -107,7 +111,8 @@ export async function getBtcMempoolTool() {
|
|
|
107
111
|
}],
|
|
108
112
|
};
|
|
109
113
|
}
|
|
110
|
-
catch {
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.warn('MarketSentiment', 'BTC mempool fetch failed', { error: error.message });
|
|
111
116
|
return {
|
|
112
117
|
content: [{ type: "text", text: "BTC Mempool: Data temporarily unavailable." }],
|
|
113
118
|
};
|
|
@@ -121,7 +126,7 @@ export async function getDefiTvlTool(_id, params) {
|
|
|
121
126
|
const chain = params.chain?.toLowerCase();
|
|
122
127
|
try {
|
|
123
128
|
if (chain) {
|
|
124
|
-
const response = await
|
|
129
|
+
const response = await resilientFetch(`https://api.llama.fi/v2/historicalChainTvl/${chain}`, { timeout: 10_000 });
|
|
125
130
|
if (!response.ok)
|
|
126
131
|
return fallbackTvl(chain);
|
|
127
132
|
const data = await response.json();
|
|
@@ -134,7 +139,7 @@ export async function getDefiTvlTool(_id, params) {
|
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
else {
|
|
137
|
-
const response = await
|
|
142
|
+
const response = await resilientFetch("https://api.llama.fi/v2/chains", { timeout: 10_000 });
|
|
138
143
|
if (!response.ok)
|
|
139
144
|
return fallbackTvl("all");
|
|
140
145
|
const data = await response.json();
|
|
@@ -148,7 +153,8 @@ export async function getDefiTvlTool(_id, params) {
|
|
|
148
153
|
};
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
|
-
catch {
|
|
156
|
+
catch (error) {
|
|
157
|
+
logger.warn('MarketSentiment', 'DeFi TVL fetch failed', { chain, error: error.message });
|
|
152
158
|
return fallbackTvl(chain ?? "all");
|
|
153
159
|
}
|
|
154
160
|
}
|
|
@@ -168,7 +174,9 @@ export async function getSolanaStatsTool() {
|
|
|
168
174
|
method: "getEpochInfo",
|
|
169
175
|
params: [],
|
|
170
176
|
});
|
|
171
|
-
const response = await
|
|
177
|
+
const response = await resilientFetch(rpcUrl, {
|
|
178
|
+
timeout: 10_000,
|
|
179
|
+
retries: 1,
|
|
172
180
|
method: "POST",
|
|
173
181
|
headers: { "Content-Type": "application/json" },
|
|
174
182
|
body,
|
|
@@ -179,7 +187,7 @@ export async function getSolanaStatsTool() {
|
|
|
179
187
|
const epoch = data.result;
|
|
180
188
|
const epochProgress = ((epoch.slotIndex / epoch.slotsInEpoch) * 100).toFixed(1);
|
|
181
189
|
// Also get SOL price
|
|
182
|
-
const priceResp = await
|
|
190
|
+
const priceResp = await resilientFetch("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true", { timeout: 8_000 });
|
|
183
191
|
let solPrice = "";
|
|
184
192
|
if (priceResp.ok) {
|
|
185
193
|
const priceData = await priceResp.json();
|
|
@@ -199,7 +207,8 @@ export async function getSolanaStatsTool() {
|
|
|
199
207
|
}],
|
|
200
208
|
};
|
|
201
209
|
}
|
|
202
|
-
catch {
|
|
210
|
+
catch (error) {
|
|
211
|
+
logger.warn('MarketSentiment', 'Solana stats fetch failed', { error: error.message });
|
|
203
212
|
return fallbackSolanaStats();
|
|
204
213
|
}
|
|
205
214
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Uses public RSS feeds and a basic lexicon-based sentiment analyzer.
|
|
4
4
|
*/
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { resilientFetch } from "../util/resilientFetch.js";
|
|
7
|
+
import { logger } from "../util/logger.js";
|
|
6
8
|
const POSITIVE_WORDS = new Set([
|
|
7
9
|
"bullish", "surge", "rally", "gain", "green", "high", "breakthrough", "adoption",
|
|
8
10
|
"partnership", "launch", "upgrade", "growth", "profit", "positive", "optimistic",
|
|
@@ -51,7 +53,9 @@ export class NewsFeed {
|
|
|
51
53
|
const url = apiKey
|
|
52
54
|
? `https://cryptopanic.com/api/v1/posts/?auth_token=${apiKey}&kind=news&public=true`
|
|
53
55
|
: "https://cryptopanic.com/api/v1/posts/?public=true";
|
|
54
|
-
const response = await
|
|
56
|
+
const response = await resilientFetch(url, {
|
|
57
|
+
timeout: 10_000,
|
|
58
|
+
retries: 1,
|
|
55
59
|
headers: { "Accept": "application/json" },
|
|
56
60
|
});
|
|
57
61
|
if (response.ok) {
|
|
@@ -71,7 +75,8 @@ export class NewsFeed {
|
|
|
71
75
|
items.push(...this.getFallbackNews());
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
|
-
catch {
|
|
78
|
+
catch (error) {
|
|
79
|
+
logger.warn('NewsSentiment', 'Failed to fetch news', { error: error.message });
|
|
75
80
|
items.push(...this.getFallbackNews());
|
|
76
81
|
}
|
|
77
82
|
this.cachedItems = items;
|
|
@@ -109,7 +114,8 @@ export class NewsFeed {
|
|
|
109
114
|
const emoji = report.averageSentiment > 0.2 ? "📈" : report.averageSentiment < -0.2 ? "📉" : "📊";
|
|
110
115
|
return `${emoji} ${(report.averageSentiment * 100).toFixed(0)}%`;
|
|
111
116
|
}
|
|
112
|
-
catch {
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.debug('NewsSentiment', 'statusBadge failed', { error: error.message });
|
|
113
119
|
return "";
|
|
114
120
|
}
|
|
115
121
|
}
|
package/dist/tools/PriceFeed.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Secondary: query any token by address.
|
|
5
5
|
*/
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { resilientFetch } from "../util/resilientFetch.js";
|
|
8
|
+
import { logger } from "../util/logger.js";
|
|
7
9
|
const AIAIAI_TOKEN = "AVPJS61gZmWKtaEpb7qYPKo8Fk2xQUsayYQxPiPMpump";
|
|
8
10
|
export class PriceFeed {
|
|
9
11
|
cachedAiaiPrice = null;
|
|
@@ -18,7 +20,7 @@ export class PriceFeed {
|
|
|
18
20
|
async fetchToken(tokenAddress) {
|
|
19
21
|
const url = `https://api.dexscreener.com/tokens/v1/solana/${tokenAddress}`;
|
|
20
22
|
try {
|
|
21
|
-
const response = await
|
|
23
|
+
const response = await resilientFetch(url, { timeout: 10_000, retries: 1 });
|
|
22
24
|
if (!response.ok) {
|
|
23
25
|
return this.fallbackPrice(tokenAddress);
|
|
24
26
|
}
|
|
@@ -48,7 +50,8 @@ export class PriceFeed {
|
|
|
48
50
|
}
|
|
49
51
|
return result;
|
|
50
52
|
}
|
|
51
|
-
catch {
|
|
53
|
+
catch (error) {
|
|
54
|
+
logger.warn('PriceFeed', 'fetchToken failed', { tokenAddress, error: error.message });
|
|
52
55
|
return this.fallbackPrice(tokenAddress);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -71,7 +74,7 @@ export class PriceFeed {
|
|
|
71
74
|
}
|
|
72
75
|
tickerLine(maxLength = 6) {
|
|
73
76
|
try {
|
|
74
|
-
const p = this.cachedAiaiPrice
|
|
77
|
+
const p = this.cachedAiaiPrice;
|
|
75
78
|
if (!p || !p.priceUsd)
|
|
76
79
|
return "";
|
|
77
80
|
const price = parseFloat(p.priceUsd).toFixed(6);
|
|
@@ -79,7 +82,9 @@ export class PriceFeed {
|
|
|
79
82
|
const arrow = change > 0 ? "▲" : change < 0 ? "▼" : "─";
|
|
80
83
|
return `$AIAIAI $${price} ${arrow}${Math.abs(change).toFixed(2)}%`.slice(0, 40);
|
|
81
84
|
}
|
|
82
|
-
catch {
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Log error in case of debug needed
|
|
87
|
+
console.error('Error in tickerLine:', error);
|
|
83
88
|
return "";
|
|
84
89
|
}
|
|
85
90
|
}
|
|
@@ -131,4 +136,6 @@ export class PriceFeed {
|
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
export const priceFeed = new PriceFeed();
|
|
139
|
+
// Eagerly initialize the price cache so sidebar shows price immediately
|
|
140
|
+
priceFeed.getAiaiaiPrice().catch(() => { });
|
|
134
141
|
//# sourceMappingURL=PriceFeed.js.map
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Works with OHLCV candle data. Tools for the agent to query.
|
|
4
4
|
*/
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { resilientFetch } from "../util/resilientFetch.js";
|
|
6
7
|
// ── SMA ──────────────────────────────────────────────────────────────────────
|
|
7
8
|
export function sma(data, period) {
|
|
8
9
|
const result = [];
|
|
@@ -197,7 +198,7 @@ export async function getCandlesTool(_id, params) {
|
|
|
197
198
|
const interval = params.interval || "1d";
|
|
198
199
|
const limit = params.limit || 50;
|
|
199
200
|
try {
|
|
200
|
-
const response = await
|
|
201
|
+
const response = await resilientFetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`, { timeout: 10_000, retries: 1 });
|
|
201
202
|
if (!response.ok) {
|
|
202
203
|
return { content: [{ type: "text", text: `Failed to fetch candles for ${symbol}: ${response.status}` }] };
|
|
203
204
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/TokenCalendar.ts — Token launch calendar tracker.
|
|
3
|
+
* Uses DexScreener's latest tokens endpoint to find recent launches.
|
|
4
|
+
* Caches results for 5 minutes.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolResult } from "../api/ExtensionAPI.js";
|
|
7
|
+
export interface LaunchEvent {
|
|
8
|
+
address: string;
|
|
9
|
+
symbol: string;
|
|
10
|
+
name: string;
|
|
11
|
+
priceUsd: string | null;
|
|
12
|
+
volume24h: number;
|
|
13
|
+
liquidityUsd: number;
|
|
14
|
+
age: string;
|
|
15
|
+
chain: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const launchCalendarParams: import("@sinclair/typebox").TObject<{
|
|
18
|
+
chain: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
19
|
+
limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function launchCalendarTool(_id: string, params: Record<string, unknown>): Promise<ToolResult>;
|
|
22
|
+
/** Get sidebar-ready summary */
|
|
23
|
+
export declare function getLaunchSummary(): string;
|
|
24
|
+
//# sourceMappingURL=TokenCalendar.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/TokenCalendar.ts — Token launch calendar tracker.
|
|
3
|
+
* Uses DexScreener's latest tokens endpoint to find recent launches.
|
|
4
|
+
* Caches results for 5 minutes.
|
|
5
|
+
*/
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { resilientFetch } from "../util/resilientFetch.js";
|
|
8
|
+
import { logger } from "../util/logger.js";
|
|
9
|
+
export const launchCalendarParams = Type.Object({
|
|
10
|
+
chain: Type.Optional(Type.String({ description: "Chain: solana, ethereum, bsc, base", default: "solana" })),
|
|
11
|
+
limit: Type.Optional(Type.Number({ description: "Max entries", default: 10 })),
|
|
12
|
+
});
|
|
13
|
+
let cachedLaunches = [];
|
|
14
|
+
let lastFetch = 0;
|
|
15
|
+
const CACHE_DURATION = 300_000; // 5 min
|
|
16
|
+
export async function launchCalendarTool(_id, params) {
|
|
17
|
+
const chain = params.chain || "solana";
|
|
18
|
+
const limit = params.limit || 10;
|
|
19
|
+
if (Date.now() - lastFetch < CACHE_DURATION && cachedLaunches.length > 0) {
|
|
20
|
+
const filtered = cachedLaunches.filter(l => l.chain === chain).slice(0, limit);
|
|
21
|
+
return formatResult(filtered, chain);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
// DexScreener "latest" endpoint per chain
|
|
25
|
+
const url = `https://api.dexscreener.com/latest/dex/tokens/${chain}`;
|
|
26
|
+
const response = await resilientFetch(url, { timeout: 10_000, retries: 1 });
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
return { content: [{ type: "text", text: `Token launch calendar unavailable for ${chain}.` }] };
|
|
29
|
+
}
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
const pairs = data?.pairs ?? [];
|
|
32
|
+
cachedLaunches = pairs.map((p) => {
|
|
33
|
+
const createdAt = p.pairCreatedAt ? new Date(p.pairCreatedAt) : null;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const diffMs = createdAt ? now - createdAt.getTime() : Infinity;
|
|
36
|
+
const age = createdAt
|
|
37
|
+
? diffMs < 3600000 ? `${Math.floor(diffMs / 60000)}m ago`
|
|
38
|
+
: diffMs < 86400000 ? `${Math.floor(diffMs / 3600000)}h ago`
|
|
39
|
+
: `${Math.floor(diffMs / 86400000)}d ago`
|
|
40
|
+
: "unknown";
|
|
41
|
+
return {
|
|
42
|
+
address: p.baseToken?.address ?? "",
|
|
43
|
+
symbol: p.baseToken?.symbol ?? "???",
|
|
44
|
+
name: p.baseToken?.name ?? "Unknown",
|
|
45
|
+
priceUsd: p.priceUsd ?? null,
|
|
46
|
+
volume24h: p.volume?.h24 ?? 0,
|
|
47
|
+
liquidityUsd: p.liquidity?.usd ?? 0,
|
|
48
|
+
age,
|
|
49
|
+
chain,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
lastFetch = Date.now();
|
|
53
|
+
const filtered = cachedLaunches.slice(0, limit);
|
|
54
|
+
return formatResult(filtered, chain);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.warn("TokenCalendar", "Failed to fetch launches", { chain, error: error.message });
|
|
58
|
+
return { content: [{ type: "text", text: `Token launch calendar: Data temporarily unavailable.` }] };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function formatResult(launches, chain) {
|
|
62
|
+
if (launches.length === 0) {
|
|
63
|
+
return { content: [{ type: "text", text: `No recent launches found on ${chain}.` }] };
|
|
64
|
+
}
|
|
65
|
+
const lines = [`🚀 Recent Token Launches — ${chain.toUpperCase()}`, ""];
|
|
66
|
+
for (const l of launches.slice(0, 10)) {
|
|
67
|
+
const price = l.priceUsd ? `$${parseFloat(l.priceUsd).toFixed(8)}` : "N/A";
|
|
68
|
+
const vol = l.volume24h > 0 ? `$${(l.volume24h / 1000).toFixed(1)}k` : "no vol";
|
|
69
|
+
const liq = l.liquidityUsd > 0 ? `$${(l.liquidityUsd / 1000).toFixed(1)}k` : "no liq";
|
|
70
|
+
lines.push(` ${l.symbol.padEnd(10)} ${price.padEnd(16)} ${vol.padEnd(10)} ${l.age.padEnd(10)} ${l.address.slice(0, 6)}…`);
|
|
71
|
+
}
|
|
72
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { launches } };
|
|
73
|
+
}
|
|
74
|
+
/** Get sidebar-ready summary */
|
|
75
|
+
export function getLaunchSummary() {
|
|
76
|
+
if (cachedLaunches.length === 0)
|
|
77
|
+
return "Loading…";
|
|
78
|
+
const recent = cachedLaunches.slice(0, 3);
|
|
79
|
+
return recent.map(l => l.symbol).join(" · ");
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=TokenCalendar.js.map
|