@aiaiaichain/agent 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/cli.js +224 -9
  2. package/dist/core/ChainConfig.js +1 -1
  3. package/dist/core/SystemMonitor.d.ts +1 -4
  4. package/dist/core/SystemMonitor.js +20 -46
  5. package/dist/index.d.ts +10 -0
  6. package/dist/index.js +11 -0
  7. package/dist/models/ModelRegistry.js +12 -4
  8. package/dist/runner/AgentRunner.d.ts +2 -0
  9. package/dist/runner/AgentRunner.js +18 -1
  10. package/dist/runner/ModelClient.js +109 -48
  11. package/dist/session/SessionManager.d.ts +1 -0
  12. package/dist/session/SessionManager.js +8 -2
  13. package/dist/session/SessionStore.js +8 -3
  14. package/dist/tools/CrossTools.js +13 -5
  15. package/dist/tools/MarketSentiment.js +22 -13
  16. package/dist/tools/NewsSentiment.js +9 -3
  17. package/dist/tools/PriceFeed.js +11 -4
  18. package/dist/tools/TechnicalAnalysis.js +2 -1
  19. package/dist/tools/TokenCalendar.d.ts +24 -0
  20. package/dist/tools/TokenCalendar.js +81 -0
  21. package/dist/tools/TokenSecurityScanner.d.ts +22 -0
  22. package/dist/tools/TokenSecurityScanner.js +102 -0
  23. package/dist/tools/TransactionSim.d.ts +17 -0
  24. package/dist/tools/TransactionSim.js +78 -0
  25. package/dist/tui/App.js +145 -23
  26. package/dist/tui/REPL.js +2 -2
  27. package/dist/tui/Sparkline.d.ts +21 -0
  28. package/dist/tui/Sparkline.js +44 -0
  29. package/dist/tui/ThemePresets.d.ts +25 -0
  30. package/dist/tui/ThemePresets.js +117 -0
  31. package/dist/util/clipboard.d.ts +9 -0
  32. package/dist/util/clipboard.js +26 -0
  33. package/dist/util/commandSuggest.d.ts +7 -0
  34. package/dist/util/commandSuggest.js +44 -0
  35. package/dist/util/confirmation.d.ts +6 -0
  36. package/dist/util/confirmation.js +16 -0
  37. package/dist/util/errorHandler.d.ts +3 -0
  38. package/dist/util/errorHandler.js +28 -0
  39. package/dist/util/logger.d.ts +11 -0
  40. package/dist/util/logger.js +43 -0
  41. package/dist/util/processManager.d.ts +5 -0
  42. package/dist/util/processManager.js +39 -0
  43. package/dist/util/resilientFetch.d.ts +21 -0
  44. package/dist/util/resilientFetch.js +94 -0
  45. package/dist/util/responseCache.d.ts +27 -0
  46. package/dist/util/responseCache.js +54 -0
  47. package/dist/util/safeLog.d.ts +4 -5
  48. package/dist/util/safeLog.js +68 -30
  49. package/dist/util/scheduler.d.ts +14 -0
  50. package/dist/util/scheduler.js +75 -0
  51. package/dist/util/webhooks.d.ts +9 -0
  52. package/dist/util/webhooks.js +75 -0
  53. package/dist/wallet/ActionFeed.d.ts +3 -2
  54. package/dist/wallet/ActionFeed.js +97 -76
  55. package/dist/wallet/AgentWallet.d.ts +6 -2
  56. package/dist/wallet/AgentWallet.js +59 -27
  57. package/dist/wallet/ProfitTracker.d.ts +30 -0
  58. package/dist/wallet/ProfitTracker.js +93 -0
  59. package/package.json +2 -2
@@ -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
- try {
28
- const response = await fetch(`${config.baseUrl}/chat/completions`, {
29
- method: "POST",
30
- headers: {
31
- "Content-Type": "application/json",
32
- Authorization: `Bearer ${config.apiKey}`,
33
- "HTTP-Referer": "https://aiaiaichain.dev",
34
- "X-Title": "AIAIAI Chain Agent",
35
- },
36
- body: JSON.stringify(body),
37
- signal,
38
- });
39
- if (!response.ok) {
40
- const err = await response.text().catch(() => "unknown error");
41
- onEvent({ type: "error", message: `API error ${response.status}: ${err}` });
42
- return;
43
- }
44
- const reader = response.body?.getReader();
45
- if (!reader) {
46
- onEvent({ type: "error", message: "No response body" });
47
- return;
48
- }
49
- const decoder = new TextDecoder();
50
- let buffer = "";
51
- while (true) {
52
- const { done, value } = await reader.read();
53
- if (done)
54
- break;
55
- buffer += decoder.decode(value, { stream: true });
56
- const lines = buffer.split("\n");
57
- buffer = lines.pop() || "";
58
- for (const line of lines) {
59
- const trimmed = line.trim();
60
- if (!trimmed || trimmed === "data: [DONE]")
61
- continue;
62
- if (!trimmed.startsWith("data: "))
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
- catch { /* skip malformed lines */ }
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
@@ -10,6 +10,7 @@ export declare class SessionManager {
10
10
  private messages;
11
11
  private systemPrompt;
12
12
  private maxContextTokens;
13
+ private _cachedChars;
13
14
  setSystemPrompt(prompt: string): void;
14
15
  getSystemPrompt(): string;
15
16
  addMessage(role: string, content: string): void;
@@ -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 { /* skip bad files */ }
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
  }
@@ -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 * 150).toFixed(2)} est)`,
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 * 150 + result.usdc).toFixed(2)} + $AIAIAI`,
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 fetch("https://api.alternative.me/fng/?limit=7");
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 fetch("https://fapi.binance.com/fapi/v1/premiumIndex");
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
- fetch("https://mempool.space/api/v1/fees/recommended"),
88
- fetch("https://mempool.space/api/mempool"),
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 fetch(`https://api.llama.fi/v2/historicalChainTvl/${chain}`);
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 fetch("https://api.llama.fi/v2/chains");
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 fetch(rpcUrl, {
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 fetch("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true");
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 fetch(url, {
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
  }
@@ -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 fetch(url);
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 ?? this.getAiaiaiPriceSync();
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 fetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`);
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