@moly-mcp/lido 1.0.6 → 1.0.7
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/alerts-ARQAPRIT.js +16 -0
- package/dist/bin.js +278 -15
- package/dist/chunk-6F64RPQQ.js +65 -0
- package/dist/chunk-6UIRFWG4.js +73 -0
- package/dist/chunk-CH4MXPWS.js +41 -0
- package/dist/chunk-CQ6ZSCMZ.js +97 -0
- package/dist/chunk-EKZFGIVK.js +289 -0
- package/dist/chunk-EQYEWCQO.js +233 -0
- package/dist/chunk-GL6TLHSF.js +215 -0
- package/dist/{chunk-PIFEXJ56.js → chunk-P6VFMSPM.js} +19 -2
- package/dist/chunk-PDX44BCA.js +11 -0
- package/dist/chunk-RR74UAKD.js +163 -0
- package/dist/daemon-GU45VVNU.js +214 -0
- package/dist/position-LKVHTEKX.js +10 -0
- package/dist/server/index.js +190 -12
- package/dist/{session-RFQTJ6WZ.js → session-2BM4AYLG.js} +325 -30
- package/dist/store-5CEITPDY.js +14 -0
- package/dist/store-SKFUVSK4.js +19 -0
- package/dist/store-WRLUM7OW.js +16 -0
- package/package.json +6 -1
- package/dist/chunk-RE3UIDLV.js +0 -545
package/dist/server/index.js
CHANGED
|
@@ -1,32 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getSettings,
|
|
4
|
+
stakeEth,
|
|
5
|
+
updateSettings
|
|
6
|
+
} from "../chunk-CQ6ZSCMZ.js";
|
|
2
7
|
import {
|
|
3
8
|
castVote,
|
|
4
9
|
claimWithdrawals,
|
|
5
|
-
getBalance,
|
|
6
|
-
getConversionRate,
|
|
7
10
|
getProposal,
|
|
8
11
|
getProposals,
|
|
9
|
-
getRewards,
|
|
10
|
-
getSettings,
|
|
11
12
|
getWithdrawalRequests,
|
|
12
13
|
getWithdrawalStatus,
|
|
13
|
-
requestWithdrawal
|
|
14
|
-
|
|
14
|
+
requestWithdrawal
|
|
15
|
+
} from "../chunk-EQYEWCQO.js";
|
|
16
|
+
import {
|
|
17
|
+
configureAlertChannels,
|
|
18
|
+
listAlerts,
|
|
19
|
+
removeAlertById,
|
|
20
|
+
setAlert
|
|
21
|
+
} from "../chunk-6UIRFWG4.js";
|
|
22
|
+
import "../chunk-6F64RPQQ.js";
|
|
23
|
+
import {
|
|
24
|
+
bridgeToEthereum,
|
|
25
|
+
getBridgeQuote,
|
|
26
|
+
getBridgeStatus,
|
|
27
|
+
getL2Balance,
|
|
28
|
+
getTotalPosition
|
|
29
|
+
} from "../chunk-GL6TLHSF.js";
|
|
30
|
+
import {
|
|
31
|
+
getBalance,
|
|
32
|
+
getConversionRate,
|
|
33
|
+
getRewards,
|
|
15
34
|
unwrapWsteth,
|
|
16
|
-
updateSettings,
|
|
17
35
|
wrapSteth
|
|
18
|
-
} from "../chunk-
|
|
36
|
+
} from "../chunk-EKZFGIVK.js";
|
|
19
37
|
import {
|
|
20
38
|
loadConfig
|
|
21
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-P6VFMSPM.js";
|
|
40
|
+
import {
|
|
41
|
+
loadBounds,
|
|
42
|
+
saveBounds
|
|
43
|
+
} from "../chunk-CH4MXPWS.js";
|
|
44
|
+
import {
|
|
45
|
+
initLedger,
|
|
46
|
+
ledgerStats,
|
|
47
|
+
queryLedger
|
|
48
|
+
} from "../chunk-RR74UAKD.js";
|
|
49
|
+
import "../chunk-PDX44BCA.js";
|
|
22
50
|
|
|
23
51
|
// src/server/index.ts
|
|
24
52
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
25
53
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26
54
|
import { z } from "zod";
|
|
55
|
+
try {
|
|
56
|
+
initLedger();
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
27
59
|
var cfg = loadConfig();
|
|
28
|
-
var modeNote = cfg.mode === "simulation" ? "
|
|
29
|
-
var server = new McpServer({ name: "@moly/lido", version: "1.0.0" });
|
|
60
|
+
var modeNote = cfg.mode === "simulation" ? "SIMULATION \u2014 dry_run true by default, no real transactions" : "LIVE \u2014 real transactions on " + (cfg.network === "mainnet" ? "Ethereum Mainnet" : "Hoodi Testnet");
|
|
61
|
+
var server = new McpServer({ name: "@moly-mcp/lido", version: "1.0.0" });
|
|
30
62
|
server.tool(
|
|
31
63
|
"get_balance",
|
|
32
64
|
`Get ETH, stETH, and wstETH balances for an address. ${modeNote}`,
|
|
@@ -174,7 +206,153 @@ server.tool(
|
|
|
174
206
|
content: [{ type: "text", text: JSON.stringify(updateSettings({ network, mode, rpc, model }), null, 2) }]
|
|
175
207
|
})
|
|
176
208
|
);
|
|
209
|
+
server.tool(
|
|
210
|
+
"set_alert",
|
|
211
|
+
"Create a new alert. Conditions: balance_below, balance_above, reward_rate_below, reward_rate_above, withdrawal_ready, proposal_new, conversion_rate_above, conversion_rate_below. Default channel: telegram.",
|
|
212
|
+
{
|
|
213
|
+
condition: z.string().describe("Alert condition type"),
|
|
214
|
+
threshold: z.number().optional().describe("Numeric threshold (required for _above/_below conditions)"),
|
|
215
|
+
channel: z.enum(["telegram", "webhook"]).optional().default("telegram").describe("Notification channel")
|
|
216
|
+
},
|
|
217
|
+
async ({ condition, threshold, channel }) => ({
|
|
218
|
+
content: [{ type: "text", text: JSON.stringify(setAlert({ condition, threshold, channel }), null, 2) }]
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
server.tool(
|
|
222
|
+
"list_alerts",
|
|
223
|
+
"List all configured alerts.",
|
|
224
|
+
{},
|
|
225
|
+
async () => ({
|
|
226
|
+
content: [{ type: "text", text: JSON.stringify(listAlerts(), null, 2) }]
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
server.tool(
|
|
230
|
+
"remove_alert",
|
|
231
|
+
"Remove an alert by ID.",
|
|
232
|
+
{ id: z.string().describe("Alert ID to remove") },
|
|
233
|
+
async ({ id }) => ({
|
|
234
|
+
content: [{ type: "text", text: JSON.stringify(removeAlertById(id), null, 2) }]
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
server.tool(
|
|
238
|
+
"configure_alert_channels",
|
|
239
|
+
"Configure Telegram and/or webhook notification channels for alerts.",
|
|
240
|
+
{
|
|
241
|
+
telegram_token: z.string().optional().describe("Telegram bot token"),
|
|
242
|
+
telegram_chat_id: z.string().optional().describe("Telegram chat ID"),
|
|
243
|
+
webhook_url: z.string().optional().describe("Webhook URL for HTTP POST notifications")
|
|
244
|
+
},
|
|
245
|
+
async ({ telegram_token, telegram_chat_id, webhook_url }) => ({
|
|
246
|
+
content: [{ type: "text", text: JSON.stringify(configureAlertChannels({ telegram_token, telegram_chat_id, webhook_url }), null, 2) }]
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
server.tool(
|
|
250
|
+
"get_total_position",
|
|
251
|
+
"Aggregated cross-chain position: ETH, stETH, wstETH across Ethereum + Base + Arbitrum, converted to ETH equivalent.",
|
|
252
|
+
{ address: z.string().optional().describe("Ethereum address (defaults to configured wallet)") },
|
|
253
|
+
async ({ address }) => ({
|
|
254
|
+
content: [{ type: "text", text: JSON.stringify(await getTotalPosition(address), null, 2) }]
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
server.tool(
|
|
258
|
+
"get_bounds",
|
|
259
|
+
"Get current policy bounds (max stake per tx, daily limit, min ETH reserve, governance auto-vote).",
|
|
260
|
+
{},
|
|
261
|
+
async () => ({
|
|
262
|
+
content: [{ type: "text", text: JSON.stringify(loadBounds(), null, 2) }]
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
server.tool(
|
|
266
|
+
"set_bounds",
|
|
267
|
+
"Update policy bounds that gate write operations.",
|
|
268
|
+
{
|
|
269
|
+
maxStakePerTx: z.number().optional().describe("Max ETH per single stake"),
|
|
270
|
+
maxDailyStake: z.number().optional().describe("Max ETH staked per day"),
|
|
271
|
+
minEthReserve: z.number().optional().describe("Min ETH to keep unstaked for gas"),
|
|
272
|
+
autoRestakeThreshold: z.number().optional().describe("Auto-restake rewards threshold"),
|
|
273
|
+
governanceAutoVote: z.boolean().optional().describe("Allow agent to auto-vote")
|
|
274
|
+
},
|
|
275
|
+
async (patch) => {
|
|
276
|
+
const current = loadBounds();
|
|
277
|
+
if (patch.maxStakePerTx !== void 0) current.maxStakePerTx = patch.maxStakePerTx;
|
|
278
|
+
if (patch.maxDailyStake !== void 0) current.maxDailyStake = patch.maxDailyStake;
|
|
279
|
+
if (patch.minEthReserve !== void 0) current.minEthReserve = patch.minEthReserve;
|
|
280
|
+
if (patch.autoRestakeThreshold !== void 0) current.autoRestakeThreshold = patch.autoRestakeThreshold;
|
|
281
|
+
if (patch.governanceAutoVote !== void 0) current.governanceAutoVote = patch.governanceAutoVote;
|
|
282
|
+
saveBounds(current);
|
|
283
|
+
return { content: [{ type: "text", text: JSON.stringify(current, null, 2) }] };
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
server.tool(
|
|
287
|
+
"get_l2_balance",
|
|
288
|
+
"Get ETH and wstETH balances on Base or Arbitrum. Mainnet only. Use this before bridging to check available funds.",
|
|
289
|
+
{
|
|
290
|
+
source_chain: z.enum(["base", "arbitrum"]).describe("L2 chain to query"),
|
|
291
|
+
address: z.string().optional().describe("Address to check (defaults to configured wallet)")
|
|
292
|
+
},
|
|
293
|
+
async ({ source_chain, address }) => ({
|
|
294
|
+
content: [{ type: "text", text: JSON.stringify(await getL2Balance(source_chain, address), null, 2) }]
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
server.tool(
|
|
298
|
+
"get_bridge_quote",
|
|
299
|
+
"Get a quote for bridging ETH or wstETH from an L2 to Ethereum L1 via LI.FI. Mainnet only. Requires a configured wallet address.",
|
|
300
|
+
{
|
|
301
|
+
source_chain: z.enum(["base", "arbitrum"]).describe("L2 to bridge from"),
|
|
302
|
+
token: z.enum(["ETH", "wstETH"]).describe("Token to bridge"),
|
|
303
|
+
amount: z.string().describe('Amount to bridge (e.g. "0.1")'),
|
|
304
|
+
to_token: z.enum(["ETH", "wstETH"]).optional().describe("Token to receive on L1 (default ETH)")
|
|
305
|
+
},
|
|
306
|
+
async ({ source_chain, token, amount, to_token }) => ({
|
|
307
|
+
content: [{ type: "text", text: JSON.stringify(await getBridgeQuote(source_chain, token, amount, to_token), null, 2) }]
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
server.tool(
|
|
311
|
+
"bridge_to_ethereum",
|
|
312
|
+
"Bridge ETH or wstETH from Base/Arbitrum to Ethereum L1 via LI.FI. Mainnet only. Requires a private key. In simulation mode this returns a quote without broadcasting.",
|
|
313
|
+
{
|
|
314
|
+
source_chain: z.enum(["base", "arbitrum"]).describe("L2 to bridge from"),
|
|
315
|
+
token: z.enum(["ETH", "wstETH"]).describe("Token to bridge"),
|
|
316
|
+
amount: z.string().describe("Amount to bridge"),
|
|
317
|
+
to_token: z.enum(["ETH", "wstETH"]).optional().describe("Token to receive on L1 (default ETH)"),
|
|
318
|
+
dry_run: z.boolean().optional().describe("Simulate without broadcasting")
|
|
319
|
+
},
|
|
320
|
+
async ({ source_chain, token, amount, to_token, dry_run }) => ({
|
|
321
|
+
content: [{ type: "text", text: JSON.stringify(await bridgeToEthereum(source_chain, token, amount, to_token, dry_run), null, 2) }]
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
server.tool(
|
|
325
|
+
"get_bridge_status",
|
|
326
|
+
"Check the status of an in-progress bridge transaction. Mainnet only. Use the tx hash returned by bridge_to_ethereum.",
|
|
327
|
+
{
|
|
328
|
+
tx_hash: z.string().describe("Bridge transaction hash on the L2"),
|
|
329
|
+
source_chain: z.enum(["base", "arbitrum"]).describe("L2 the bridge was sent from")
|
|
330
|
+
},
|
|
331
|
+
async ({ tx_hash, source_chain }) => ({
|
|
332
|
+
content: [{ type: "text", text: JSON.stringify(await getBridgeStatus(tx_hash, source_chain), null, 2) }]
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
server.tool(
|
|
336
|
+
"get_trade_history",
|
|
337
|
+
"Query the activity ledger with filters.",
|
|
338
|
+
{
|
|
339
|
+
tool: z.string().optional().describe("Filter by tool name (e.g. stake_eth)"),
|
|
340
|
+
since: z.string().optional().describe("ISO date to filter from"),
|
|
341
|
+
limit: z.number().int().optional().default(50).describe("Max results")
|
|
342
|
+
},
|
|
343
|
+
async (opts) => ({
|
|
344
|
+
content: [{ type: "text", text: JSON.stringify(queryLedger(opts), null, 2) }]
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
server.tool(
|
|
348
|
+
"get_staking_summary",
|
|
349
|
+
"Aggregate stats from the activity ledger: total operations, staked ETH, errors.",
|
|
350
|
+
{ since: z.string().optional().describe("ISO date to filter from") },
|
|
351
|
+
async ({ since }) => ({
|
|
352
|
+
content: [{ type: "text", text: JSON.stringify(ledgerStats(since), null, 2) }]
|
|
353
|
+
})
|
|
354
|
+
);
|
|
177
355
|
var transport = new StdioServerTransport();
|
|
178
356
|
await server.connect(transport);
|
|
179
|
-
process.stderr.write(`@moly/lido MCP server started \u2014 ${modeNote}
|
|
357
|
+
process.stderr.write(`@moly-mcp/lido MCP server started \u2014 ${modeNote}
|
|
180
358
|
`);
|
|
@@ -1,27 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getSettings,
|
|
4
|
+
stakeEth,
|
|
5
|
+
updateSettings
|
|
6
|
+
} from "./chunk-CQ6ZSCMZ.js";
|
|
2
7
|
import {
|
|
3
8
|
castVote,
|
|
4
9
|
claimWithdrawals,
|
|
5
|
-
getBalance,
|
|
6
|
-
getConversionRate,
|
|
7
10
|
getProposal,
|
|
8
11
|
getProposals,
|
|
9
|
-
getRewards,
|
|
10
|
-
getSettings,
|
|
11
12
|
getWithdrawalRequests,
|
|
12
13
|
getWithdrawalStatus,
|
|
13
|
-
requestWithdrawal
|
|
14
|
-
|
|
14
|
+
requestWithdrawal
|
|
15
|
+
} from "./chunk-EQYEWCQO.js";
|
|
16
|
+
import {
|
|
17
|
+
configureAlertChannels,
|
|
18
|
+
listAlerts,
|
|
19
|
+
removeAlertById,
|
|
20
|
+
setAlert
|
|
21
|
+
} from "./chunk-6UIRFWG4.js";
|
|
22
|
+
import "./chunk-6F64RPQQ.js";
|
|
23
|
+
import {
|
|
24
|
+
bridgeToEthereum,
|
|
25
|
+
getBridgeQuote,
|
|
26
|
+
getBridgeStatus,
|
|
27
|
+
getL2Balance,
|
|
28
|
+
getTotalPosition
|
|
29
|
+
} from "./chunk-GL6TLHSF.js";
|
|
30
|
+
import {
|
|
31
|
+
getBalance,
|
|
32
|
+
getConversionRate,
|
|
33
|
+
getRewards,
|
|
15
34
|
unwrapWsteth,
|
|
16
|
-
updateSettings,
|
|
17
35
|
wrapSteth
|
|
18
|
-
} from "./chunk-
|
|
19
|
-
import "./chunk-
|
|
36
|
+
} from "./chunk-EKZFGIVK.js";
|
|
37
|
+
import "./chunk-P6VFMSPM.js";
|
|
38
|
+
import {
|
|
39
|
+
loadBounds,
|
|
40
|
+
saveBounds
|
|
41
|
+
} from "./chunk-CH4MXPWS.js";
|
|
42
|
+
import {
|
|
43
|
+
initLedger,
|
|
44
|
+
ledgerStats,
|
|
45
|
+
logEntry,
|
|
46
|
+
queryLedger
|
|
47
|
+
} from "./chunk-RR74UAKD.js";
|
|
48
|
+
import "./chunk-PDX44BCA.js";
|
|
20
49
|
|
|
21
50
|
// src/chat/session.ts
|
|
22
51
|
import * as readline from "readline";
|
|
23
52
|
import * as fs from "fs";
|
|
24
53
|
import * as path from "path";
|
|
54
|
+
import { fileURLToPath } from "url";
|
|
25
55
|
|
|
26
56
|
// src/chat/providers.ts
|
|
27
57
|
async function callAnthropic(apiKey, model, messages, tools) {
|
|
@@ -325,14 +355,158 @@ var TOOL_DEFS = [
|
|
|
325
355
|
},
|
|
326
356
|
{
|
|
327
357
|
name: "update_settings",
|
|
328
|
-
description: "Change mode, network, or
|
|
358
|
+
description: "Change mode, network, RPC, or chain scope.",
|
|
329
359
|
parameters: {
|
|
330
360
|
type: "object",
|
|
331
361
|
properties: {
|
|
332
362
|
network: { type: "string", enum: ["hoodi", "mainnet"] },
|
|
333
363
|
mode: { type: "string", enum: ["simulation", "live"] },
|
|
334
364
|
rpc: { type: "string", nullable: true },
|
|
335
|
-
model: { type: "string" }
|
|
365
|
+
model: { type: "string" },
|
|
366
|
+
chain_scope: { type: "string", enum: ["ethereum", "all"], description: "ethereum = L1 only, all = L1 + Base/Arbitrum bridging" }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "get_l2_balance",
|
|
372
|
+
description: "Get ETH and wstETH balances on Base or Arbitrum L2. Mainnet only.",
|
|
373
|
+
parameters: {
|
|
374
|
+
type: "object",
|
|
375
|
+
required: ["source_chain"],
|
|
376
|
+
properties: {
|
|
377
|
+
source_chain: { type: "string", enum: ["base", "arbitrum"], description: "L2 chain to query" },
|
|
378
|
+
address: { type: "string", description: "Address to check (optional, defaults to wallet)" }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "get_bridge_quote",
|
|
384
|
+
description: "Get a quote for bridging ETH or wstETH from an L2 to Ethereum L1 via LI.FI. Mainnet only.",
|
|
385
|
+
parameters: {
|
|
386
|
+
type: "object",
|
|
387
|
+
required: ["source_chain", "token", "amount"],
|
|
388
|
+
properties: {
|
|
389
|
+
source_chain: { type: "string", enum: ["base", "arbitrum"] },
|
|
390
|
+
token: { type: "string", enum: ["ETH", "wstETH"], description: "Token to bridge" },
|
|
391
|
+
amount: { type: "string", description: 'Amount to bridge (e.g. "0.1")' },
|
|
392
|
+
to_token: { type: "string", enum: ["ETH", "wstETH"], description: "Token to receive on L1 (default ETH)" }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: "bridge_to_ethereum",
|
|
398
|
+
description: "Bridge ETH or wstETH from Base/Arbitrum to Ethereum L1 via LI.FI. Mainnet only.",
|
|
399
|
+
parameters: {
|
|
400
|
+
type: "object",
|
|
401
|
+
required: ["source_chain", "token", "amount"],
|
|
402
|
+
properties: {
|
|
403
|
+
source_chain: { type: "string", enum: ["base", "arbitrum"] },
|
|
404
|
+
token: { type: "string", enum: ["ETH", "wstETH"] },
|
|
405
|
+
amount: { type: "string", description: "Amount to bridge" },
|
|
406
|
+
to_token: { type: "string", enum: ["ETH", "wstETH"] },
|
|
407
|
+
dry_run: { type: "boolean", description: "Simulate without broadcasting" }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "get_bridge_status",
|
|
413
|
+
description: "Check the status of an in-progress bridge transaction. Mainnet only.",
|
|
414
|
+
parameters: {
|
|
415
|
+
type: "object",
|
|
416
|
+
required: ["tx_hash", "source_chain"],
|
|
417
|
+
properties: {
|
|
418
|
+
tx_hash: { type: "string", description: "Bridge transaction hash on the L2" },
|
|
419
|
+
source_chain: { type: "string", enum: ["base", "arbitrum"] }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "set_alert",
|
|
425
|
+
description: "Create a new alert. Conditions: balance_below, balance_above, reward_rate_below, reward_rate_above, withdrawal_ready, proposal_new, conversion_rate_above, conversion_rate_below.",
|
|
426
|
+
parameters: {
|
|
427
|
+
type: "object",
|
|
428
|
+
required: ["condition"],
|
|
429
|
+
properties: {
|
|
430
|
+
condition: { type: "string", description: "Alert condition type" },
|
|
431
|
+
threshold: { type: "number", description: "Numeric threshold (required for _above/_below conditions)" },
|
|
432
|
+
channel: { type: "string", enum: ["telegram", "webhook"], description: "Notification channel (default: telegram)" }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "list_alerts",
|
|
438
|
+
description: "List all configured alerts.",
|
|
439
|
+
parameters: { type: "object", properties: {} }
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "remove_alert",
|
|
443
|
+
description: "Remove an alert by ID.",
|
|
444
|
+
parameters: {
|
|
445
|
+
type: "object",
|
|
446
|
+
required: ["id"],
|
|
447
|
+
properties: { id: { type: "string", description: "Alert ID to remove" } }
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "configure_alert_channels",
|
|
452
|
+
description: "Configure Telegram and/or webhook notification channels for alerts.",
|
|
453
|
+
parameters: {
|
|
454
|
+
type: "object",
|
|
455
|
+
properties: {
|
|
456
|
+
telegram_token: { type: "string", description: "Telegram bot token" },
|
|
457
|
+
telegram_chat_id: { type: "string", description: "Telegram chat ID" },
|
|
458
|
+
webhook_url: { type: "string", description: "Webhook URL for HTTP POST notifications" }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "get_total_position",
|
|
464
|
+
description: "Aggregated cross-chain position: ETH, stETH, wstETH across Ethereum + Base + Arbitrum, all converted to ETH equivalent.",
|
|
465
|
+
parameters: {
|
|
466
|
+
type: "object",
|
|
467
|
+
properties: {
|
|
468
|
+
address: { type: "string", description: "Ethereum address (optional)" }
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "get_bounds",
|
|
474
|
+
description: "Get current policy bounds (max stake per tx, daily limit, min ETH reserve, governance auto-vote).",
|
|
475
|
+
parameters: { type: "object", properties: {} }
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: "set_bounds",
|
|
479
|
+
description: "Update policy bounds that gate write operations.",
|
|
480
|
+
parameters: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
maxStakePerTx: { type: "number", description: "Max ETH per single stake" },
|
|
484
|
+
maxDailyStake: { type: "number", description: "Max ETH staked per day" },
|
|
485
|
+
minEthReserve: { type: "number", description: "Min ETH to keep unstaked for gas" },
|
|
486
|
+
autoRestakeThreshold: { type: "number", description: "Auto-restake rewards threshold in ETH" },
|
|
487
|
+
governanceAutoVote: { type: "boolean", description: "Allow agent to auto-vote on proposals" }
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "get_trade_history",
|
|
493
|
+
description: "Query the activity ledger with filters.",
|
|
494
|
+
parameters: {
|
|
495
|
+
type: "object",
|
|
496
|
+
properties: {
|
|
497
|
+
tool: { type: "string", description: "Filter by tool name (e.g. stake_eth)" },
|
|
498
|
+
since: { type: "string", description: "ISO date to filter from (e.g. 2026-01-01)" },
|
|
499
|
+
limit: { type: "number", description: "Max results (default 50)" }
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "get_staking_summary",
|
|
505
|
+
description: "Aggregate stats from the activity ledger: total operations, staked ETH, errors.",
|
|
506
|
+
parameters: {
|
|
507
|
+
type: "object",
|
|
508
|
+
properties: {
|
|
509
|
+
since: { type: "string", description: "ISO date to filter from (optional)" }
|
|
336
510
|
}
|
|
337
511
|
}
|
|
338
512
|
}
|
|
@@ -386,6 +560,54 @@ async function executeTool(name, args) {
|
|
|
386
560
|
case "update_settings":
|
|
387
561
|
result = updateSettings(args);
|
|
388
562
|
break;
|
|
563
|
+
case "get_l2_balance":
|
|
564
|
+
result = await getL2Balance(args.source_chain, args.address);
|
|
565
|
+
break;
|
|
566
|
+
case "get_bridge_quote":
|
|
567
|
+
result = await getBridgeQuote(args.source_chain, args.token, args.amount, args.to_token);
|
|
568
|
+
break;
|
|
569
|
+
case "bridge_to_ethereum":
|
|
570
|
+
result = await bridgeToEthereum(args.source_chain, args.token, args.amount, args.to_token, args.dry_run);
|
|
571
|
+
break;
|
|
572
|
+
case "get_bridge_status":
|
|
573
|
+
result = await getBridgeStatus(args.tx_hash, args.source_chain);
|
|
574
|
+
break;
|
|
575
|
+
case "set_alert":
|
|
576
|
+
result = setAlert(args);
|
|
577
|
+
break;
|
|
578
|
+
case "list_alerts":
|
|
579
|
+
result = listAlerts();
|
|
580
|
+
break;
|
|
581
|
+
case "remove_alert":
|
|
582
|
+
result = removeAlertById(args.id);
|
|
583
|
+
break;
|
|
584
|
+
case "configure_alert_channels":
|
|
585
|
+
result = configureAlertChannels(args);
|
|
586
|
+
break;
|
|
587
|
+
case "get_total_position":
|
|
588
|
+
result = await getTotalPosition(args.address);
|
|
589
|
+
break;
|
|
590
|
+
case "get_bounds":
|
|
591
|
+
result = loadBounds();
|
|
592
|
+
break;
|
|
593
|
+
case "set_bounds": {
|
|
594
|
+
const current = loadBounds();
|
|
595
|
+
const patch = args;
|
|
596
|
+
if (patch.maxStakePerTx !== void 0) current.maxStakePerTx = patch.maxStakePerTx;
|
|
597
|
+
if (patch.maxDailyStake !== void 0) current.maxDailyStake = patch.maxDailyStake;
|
|
598
|
+
if (patch.minEthReserve !== void 0) current.minEthReserve = patch.minEthReserve;
|
|
599
|
+
if (patch.autoRestakeThreshold !== void 0) current.autoRestakeThreshold = patch.autoRestakeThreshold;
|
|
600
|
+
if (patch.governanceAutoVote !== void 0) current.governanceAutoVote = patch.governanceAutoVote;
|
|
601
|
+
saveBounds(current);
|
|
602
|
+
result = current;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case "get_trade_history":
|
|
606
|
+
result = queryLedger({ tool: args.tool, since: args.since, limit: args.limit });
|
|
607
|
+
break;
|
|
608
|
+
case "get_staking_summary":
|
|
609
|
+
result = ledgerStats(args.since);
|
|
610
|
+
break;
|
|
389
611
|
default:
|
|
390
612
|
return `Unknown tool: ${name}`;
|
|
391
613
|
}
|
|
@@ -395,6 +617,50 @@ async function executeTool(name, args) {
|
|
|
395
617
|
}
|
|
396
618
|
}
|
|
397
619
|
|
|
620
|
+
// src/bounds/enforce.ts
|
|
621
|
+
var STAKE_TOOLS = /* @__PURE__ */ new Set(["stake_eth", "bridge_to_ethereum"]);
|
|
622
|
+
async function checkBounds(toolName, args) {
|
|
623
|
+
const bounds = loadBounds();
|
|
624
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
625
|
+
if (bounds.lastResetDate !== today) {
|
|
626
|
+
bounds.dailyStaked = 0;
|
|
627
|
+
bounds.lastResetDate = today;
|
|
628
|
+
saveBounds(bounds);
|
|
629
|
+
}
|
|
630
|
+
if (toolName === "cast_vote" && !bounds.governanceAutoVote) {
|
|
631
|
+
return { allowed: false, reason: `Governance auto-vote is disabled. Set bounds: governanceAutoVote = true` };
|
|
632
|
+
}
|
|
633
|
+
if (STAKE_TOOLS.has(toolName)) {
|
|
634
|
+
const amount = parseFloat(args.amount_eth ?? args.amount ?? "0");
|
|
635
|
+
if (isNaN(amount) || amount <= 0) return { allowed: true };
|
|
636
|
+
if (amount > bounds.maxStakePerTx) {
|
|
637
|
+
return { allowed: false, reason: `Amount ${amount} ETH exceeds max per tx (${bounds.maxStakePerTx} ETH)` };
|
|
638
|
+
}
|
|
639
|
+
if (bounds.dailyStaked + amount > bounds.maxDailyStake) {
|
|
640
|
+
return { allowed: false, reason: `Would exceed daily limit: staked today ${bounds.dailyStaked.toFixed(4)} + ${amount} > ${bounds.maxDailyStake} ETH` };
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
const bal = await getBalance();
|
|
644
|
+
const ethBal = parseFloat(bal.balances.eth);
|
|
645
|
+
if (ethBal - amount < bounds.minEthReserve) {
|
|
646
|
+
return { allowed: false, reason: `Would leave only ${(ethBal - amount).toFixed(4)} ETH, below reserve of ${bounds.minEthReserve} ETH` };
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return { allowed: true };
|
|
652
|
+
}
|
|
653
|
+
function recordStake(amount) {
|
|
654
|
+
const bounds = loadBounds();
|
|
655
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
656
|
+
if (bounds.lastResetDate !== today) {
|
|
657
|
+
bounds.dailyStaked = 0;
|
|
658
|
+
bounds.lastResetDate = today;
|
|
659
|
+
}
|
|
660
|
+
bounds.dailyStaked += amount;
|
|
661
|
+
saveBounds(bounds);
|
|
662
|
+
}
|
|
663
|
+
|
|
398
664
|
// src/chat/session.ts
|
|
399
665
|
var R = "\x1B[0m";
|
|
400
666
|
var B = "\x1B[1m";
|
|
@@ -412,30 +678,29 @@ ${CY}${B} \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2
|
|
|
412
678
|
${CY}${B} \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D ${R}
|
|
413
679
|
${CY}${B} \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 ${R}
|
|
414
680
|
${CY}${B} \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D ${R}
|
|
415
|
-
${D} powered by Lido
|
|
681
|
+
${D} powered by Lido${R}
|
|
416
682
|
`;
|
|
417
683
|
function ln(text = "") {
|
|
418
684
|
process.stdout.write(text + "\n");
|
|
419
685
|
}
|
|
420
686
|
function saveTrade(toolName, args, result) {
|
|
421
687
|
try {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
688
|
+
let txHash;
|
|
689
|
+
let amount;
|
|
690
|
+
try {
|
|
691
|
+
const parsed = JSON.parse(result);
|
|
692
|
+
txHash = parsed.txHash ?? parsed.tx_hash;
|
|
693
|
+
amount = args.amount_eth ?? args.amount_steth ?? args.amount;
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
logEntry({
|
|
428
697
|
tool: toolName,
|
|
429
698
|
args,
|
|
430
|
-
result
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return result;
|
|
435
|
-
}
|
|
436
|
-
})()
|
|
699
|
+
result,
|
|
700
|
+
tx_hash: txHash,
|
|
701
|
+
amount,
|
|
702
|
+
status: "ok"
|
|
437
703
|
});
|
|
438
|
-
fs.appendFileSync(file, record + "\n");
|
|
439
704
|
} catch {
|
|
440
705
|
}
|
|
441
706
|
}
|
|
@@ -453,18 +718,31 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
453
718
|
"claim_withdrawals",
|
|
454
719
|
"wrap_steth",
|
|
455
720
|
"unwrap_wsteth",
|
|
456
|
-
"cast_vote"
|
|
721
|
+
"cast_vote",
|
|
722
|
+
"bridge_to_ethereum"
|
|
457
723
|
]);
|
|
458
724
|
async function startChatSession(cfg) {
|
|
459
725
|
if (!cfg.ai) {
|
|
460
726
|
ln(`${RE}No AI provider configured. Run: moly setup${R}`);
|
|
461
727
|
process.exit(1);
|
|
462
728
|
}
|
|
729
|
+
try {
|
|
730
|
+
initLedger();
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
463
733
|
const { provider, apiKey, model } = cfg.ai;
|
|
734
|
+
let skillContext = "";
|
|
735
|
+
try {
|
|
736
|
+
const skillPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../lido.skill.md");
|
|
737
|
+
if (fs.existsSync(skillPath)) {
|
|
738
|
+
skillContext = fs.readFileSync(skillPath, "utf-8") + "\n\n";
|
|
739
|
+
}
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
464
742
|
const messages = [
|
|
465
743
|
{
|
|
466
744
|
role: "user",
|
|
467
|
-
content: `You are Moly, a terminal assistant for Lido Finance on ${cfg.network}. Mode: ${cfg.mode} (${cfg.mode === "simulation" ? "dry-run, nothing broadcast" : "LIVE - real on-chain transactions"}). You can only do what your tools support: staking ETH, withdrawals, wrap/unwrap stETH/wstETH, balances, rewards,
|
|
745
|
+
content: skillContext + `You are Moly, a terminal assistant for Lido Finance on ${cfg.network}. Mode: ${cfg.mode} (${cfg.mode === "simulation" ? "dry-run, nothing broadcast" : "LIVE - real on-chain transactions"}). Chain scope: ${cfg.chainScope ?? "ethereum"}. You can only do what your tools support: staking ETH, withdrawals, wrap/unwrap stETH/wstETH, balances, rewards, Lido DAO governance${cfg.chainScope === "all" ? ", and L2 bridging from Base/Arbitrum to Ethereum via LI.FI" : ""}. ${cfg.chainScope === "all" ? "If the user wants to stake ETH from Base or Arbitrum, first check their L2 balance with get_l2_balance, then bridge to Ethereum with bridge_to_ethereum, then after bridging completes use stake_eth. Bridge takes 1-20 min, tell user to check with get_bridge_status. " : ""}If asked about anything outside those tools (e.g. Lido Vaults, validators, node operators, DeFi integrations), say clearly and briefly that it is not supported. IMPORTANT: This is a terminal. Never use markdown. No **bold**, no bullet points, no headers, no backticks. Plain text only. Be concise. For live transactions always confirm first.`
|
|
468
746
|
},
|
|
469
747
|
{
|
|
470
748
|
role: "assistant",
|
|
@@ -473,8 +751,8 @@ async function startChatSession(cfg) {
|
|
|
473
751
|
];
|
|
474
752
|
printBanner(cfg);
|
|
475
753
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
476
|
-
const prompt = () => new Promise((
|
|
477
|
-
rl.question(`${B}${BL}you${R} \u203A `,
|
|
754
|
+
const prompt = () => new Promise((resolve2, reject) => {
|
|
755
|
+
rl.question(`${B}${BL}you${R} \u203A `, resolve2);
|
|
478
756
|
rl.once("close", () => reject(new Error("closed")));
|
|
479
757
|
});
|
|
480
758
|
while (true) {
|
|
@@ -500,10 +778,27 @@ async function startChatSession(cfg) {
|
|
|
500
778
|
const toolResults = [];
|
|
501
779
|
for (const tc of response.toolCalls) {
|
|
502
780
|
ln(`${D} \u21B3 ${MA}${tc.name}${R}${D} ${JSON.stringify(tc.args)}${R}`);
|
|
781
|
+
if (WRITE_TOOLS.has(tc.name)) {
|
|
782
|
+
try {
|
|
783
|
+
const check = await checkBounds(tc.name, tc.args);
|
|
784
|
+
if (!check.allowed) {
|
|
785
|
+
ln(`${RE} \u2715 BLOCKED: ${check.reason}${R}`);
|
|
786
|
+
toolResults.push(makeToolResultMessage(provider, tc.id, tc.name, JSON.stringify({ blocked: true, reason: check.reason })));
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
503
792
|
const result = await executeTool(tc.name, tc.args);
|
|
504
793
|
ln(`${D} ${result.slice(0, 300)}${result.length > 300 ? "\u2026" : ""}${R}`);
|
|
505
794
|
if (WRITE_TOOLS.has(tc.name)) {
|
|
506
795
|
saveTrade(tc.name, tc.args, result);
|
|
796
|
+
if (tc.name === "stake_eth" && tc.args.amount_eth) {
|
|
797
|
+
try {
|
|
798
|
+
recordStake(parseFloat(tc.args.amount_eth));
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
}
|
|
507
802
|
}
|
|
508
803
|
toolResults.push(makeToolResultMessage(provider, tc.id, tc.name, result));
|
|
509
804
|
}
|