opencode-froggy 0.1.0 → 0.3.0
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/LICENSE +21 -0
- package/README.md +508 -246
- package/agent/architect.md +91 -0
- package/agent/partner.md +143 -0
- package/agent/rubber-duck.md +129 -0
- package/command/commit-push.md +21 -0
- package/command/doc-changes.md +45 -0
- package/command/review-changes.md +1 -21
- package/command/review-pr.md +1 -22
- package/command/send-to.md +21 -0
- package/command/simplify-changes.md +2 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -52
- package/dist/index.test.js +29 -8
- package/dist/loaders.d.ts +9 -5
- package/dist/loaders.js +5 -1
- package/dist/tools/blockchain/eth-address-balance.d.ts +20 -0
- package/dist/tools/blockchain/eth-address-balance.js +37 -0
- package/dist/tools/blockchain/eth-address-txs.d.ts +23 -0
- package/dist/tools/blockchain/eth-address-txs.js +41 -0
- package/dist/tools/blockchain/eth-token-transfers.d.ts +23 -0
- package/dist/tools/blockchain/eth-token-transfers.js +41 -0
- package/dist/tools/blockchain/eth-transaction.d.ts +20 -0
- package/dist/tools/blockchain/eth-transaction.js +40 -0
- package/dist/tools/blockchain/etherscan-client.d.ts +25 -0
- package/dist/tools/blockchain/etherscan-client.js +156 -0
- package/dist/tools/blockchain/etherscan-client.test.d.ts +1 -0
- package/dist/tools/blockchain/etherscan-client.test.js +211 -0
- package/dist/tools/blockchain/formatters.d.ts +10 -0
- package/dist/tools/blockchain/formatters.js +147 -0
- package/dist/tools/blockchain/index.d.ts +10 -0
- package/dist/tools/blockchain/index.js +10 -0
- package/dist/tools/blockchain/tools.test.d.ts +1 -0
- package/dist/tools/blockchain/tools.test.js +208 -0
- package/dist/tools/blockchain/types.d.ts +90 -0
- package/dist/tools/blockchain/types.js +8 -0
- package/dist/tools/diff-summary.d.ts +20 -0
- package/dist/tools/diff-summary.js +111 -0
- package/dist/tools/gitingest.d.ts +26 -0
- package/dist/tools/gitingest.js +41 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/list-child-sessions.d.ts +9 -0
- package/dist/tools/list-child-sessions.js +24 -0
- package/dist/tools/prompt-session.d.ts +19 -0
- package/dist/tools/prompt-session.js +39 -0
- package/dist/tools/reply-child.d.ts +19 -0
- package/dist/tools/reply-child.js +42 -0
- package/images/logo.png +0 -0
- package/package.json +4 -2
- package/command/commit.md +0 -18
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
1
|
import { dirname, join } from "node:path";
|
|
3
2
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { loadAgents,
|
|
3
|
+
import { loadAgents, loadCommands, loadHooks, mergeHooks, } from "./loaders";
|
|
5
4
|
import { getGlobalHookDir, getProjectHookDir } from "./config-paths";
|
|
6
5
|
import { hasCodeExtension } from "./code-files";
|
|
7
6
|
import { log } from "./logger";
|
|
8
7
|
import { executeBashAction, DEFAULT_BASH_TIMEOUT, } from "./bash-executor";
|
|
9
|
-
|
|
8
|
+
import { gitingestTool, createDiffSummaryTool, createPromptSessionTool, createListChildSessionsTool, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
|
|
9
|
+
export { parseFrontmatter, loadAgents, loadCommands } from "./loaders";
|
|
10
10
|
// ============================================================================
|
|
11
11
|
// CONSTANTS
|
|
12
12
|
// ============================================================================
|
|
@@ -14,35 +14,42 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
14
14
|
const __dirname = dirname(__filename);
|
|
15
15
|
const PLUGIN_ROOT = join(__dirname, "..");
|
|
16
16
|
const AGENT_DIR = join(PLUGIN_ROOT, "agent");
|
|
17
|
-
const SKILL_DIR = join(PLUGIN_ROOT, "skill");
|
|
18
17
|
const COMMAND_DIR = join(PLUGIN_ROOT, "command");
|
|
19
18
|
// ============================================================================
|
|
20
19
|
// PLUGIN
|
|
21
20
|
// ============================================================================
|
|
22
21
|
const SmartfrogPlugin = async (ctx) => {
|
|
23
22
|
const agents = loadAgents(AGENT_DIR);
|
|
24
|
-
const skills = loadSkills(SKILL_DIR);
|
|
25
23
|
const commands = loadCommands(COMMAND_DIR);
|
|
26
24
|
const globalHooks = loadHooks(getGlobalHookDir());
|
|
27
25
|
const projectHooks = loadHooks(getProjectHookDir(ctx.directory));
|
|
28
26
|
const hooks = mergeHooks(globalHooks, projectHooks);
|
|
29
27
|
const modifiedCodeFiles = new Map();
|
|
30
28
|
const pendingToolArgs = new Map();
|
|
31
|
-
let mainSessionID;
|
|
32
29
|
log("[init] Plugin loaded", {
|
|
33
30
|
agents: Object.keys(agents),
|
|
34
31
|
commands: Object.keys(commands),
|
|
35
|
-
skills: skills.map(s => s.name),
|
|
36
32
|
hooks: Array.from(hooks.keys()),
|
|
33
|
+
tools: [
|
|
34
|
+
"gitingest",
|
|
35
|
+
"diff-summary",
|
|
36
|
+
"eth-transaction",
|
|
37
|
+
"eth-address-txs",
|
|
38
|
+
"eth-address-balance",
|
|
39
|
+
"eth-token-transfers",
|
|
40
|
+
],
|
|
37
41
|
});
|
|
38
42
|
async function executeHookActions(hook, sessionID, extraLog, options) {
|
|
39
43
|
const prefix = `[hook:${hook.event}]`;
|
|
40
44
|
const canBlock = options?.canBlock ?? false;
|
|
41
45
|
const conditions = hook.conditions ?? [];
|
|
42
46
|
for (const condition of conditions) {
|
|
43
|
-
if (condition === "isMainSession"
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
if (condition === "isMainSession") {
|
|
48
|
+
const sessionInfo = await ctx.client.session.get({ path: { id: sessionID } });
|
|
49
|
+
if (sessionInfo.data?.parentID) {
|
|
50
|
+
log(`${prefix} condition not met, skipping`, { sessionID, condition });
|
|
51
|
+
return { blocked: false };
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
if (condition === "hasCodeChange") {
|
|
48
55
|
const files = extraLog?.files;
|
|
@@ -78,15 +85,6 @@ const SmartfrogPlugin = async (ctx) => {
|
|
|
78
85
|
});
|
|
79
86
|
log(`${prefix} command result`, { command: name, status: result.response?.status, error: result.error });
|
|
80
87
|
}
|
|
81
|
-
else if ("skill" in action) {
|
|
82
|
-
log(`${prefix} executing skill`, { skill: action.skill });
|
|
83
|
-
const result = await ctx.client.session.prompt({
|
|
84
|
-
path: { id: sessionID },
|
|
85
|
-
body: { parts: [{ type: "text", text: `Use the skill tool to load the "${action.skill}" skill and follow its instructions.` }] },
|
|
86
|
-
query: { directory: ctx.directory },
|
|
87
|
-
});
|
|
88
|
-
log(`${prefix} skill result`, { skill: action.skill, status: result.response?.status, error: result.error });
|
|
89
|
-
}
|
|
90
88
|
else if ("tool" in action) {
|
|
91
89
|
log(`${prefix} executing tool`, { tool: action.tool.name });
|
|
92
90
|
const result = await ctx.client.session.prompt({
|
|
@@ -184,28 +182,14 @@ const SmartfrogPlugin = async (ctx) => {
|
|
|
184
182
|
}
|
|
185
183
|
},
|
|
186
184
|
tool: {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const skill = skills.find((s) => s.name === args.name);
|
|
196
|
-
if (!skill) {
|
|
197
|
-
const available = skills.map((s) => s.name).join(", ");
|
|
198
|
-
throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`);
|
|
199
|
-
}
|
|
200
|
-
return [
|
|
201
|
-
`## Skill: ${skill.name}`,
|
|
202
|
-
"",
|
|
203
|
-
`**Base directory**: ${dirname(skill.path)}`,
|
|
204
|
-
"",
|
|
205
|
-
skill.body,
|
|
206
|
-
].join("\n");
|
|
207
|
-
},
|
|
208
|
-
}),
|
|
185
|
+
gitingest: gitingestTool,
|
|
186
|
+
"diff-summary": createDiffSummaryTool(ctx.directory),
|
|
187
|
+
"prompt-session": createPromptSessionTool(ctx.client),
|
|
188
|
+
"list-child-sessions": createListChildSessionsTool(ctx.client),
|
|
189
|
+
"eth-transaction": ethTransactionTool,
|
|
190
|
+
"eth-address-txs": ethAddressTxsTool,
|
|
191
|
+
"eth-address-balance": ethAddressBalanceTool,
|
|
192
|
+
"eth-token-transfers": ethTokenTransfersTool,
|
|
209
193
|
},
|
|
210
194
|
"tool.execute.before": async (input, output) => {
|
|
211
195
|
const sessionID = input.sessionID;
|
|
@@ -247,7 +231,6 @@ const SmartfrogPlugin = async (ctx) => {
|
|
|
247
231
|
if (!sessionID)
|
|
248
232
|
return;
|
|
249
233
|
if (!info.parentID) {
|
|
250
|
-
mainSessionID = sessionID;
|
|
251
234
|
log("[event] session.created - main session", { sessionID });
|
|
252
235
|
}
|
|
253
236
|
await triggerHooks("session.created", sessionID);
|
|
@@ -260,21 +243,13 @@ const SmartfrogPlugin = async (ctx) => {
|
|
|
260
243
|
log("[event] session.deleted", { sessionID });
|
|
261
244
|
await triggerHooks("session.deleted", sessionID);
|
|
262
245
|
modifiedCodeFiles.delete(sessionID);
|
|
263
|
-
if (sessionID === mainSessionID) {
|
|
264
|
-
mainSessionID = undefined;
|
|
265
|
-
}
|
|
266
246
|
}
|
|
267
247
|
if (event.type === "session.idle") {
|
|
268
248
|
const sessionID = props?.sessionID;
|
|
269
|
-
log("[event] session.idle", { sessionID, mainSessionID });
|
|
270
249
|
if (!sessionID)
|
|
271
250
|
return;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
log("[event] session.idle - setting mainSessionID from idle event", { sessionID });
|
|
275
|
-
}
|
|
276
|
-
const eventHooks = hooks.get("session.idle");
|
|
277
|
-
if (!eventHooks || eventHooks.length === 0) {
|
|
251
|
+
log("[event] session.idle", { sessionID });
|
|
252
|
+
if (!hooks.has("session.idle")) {
|
|
278
253
|
log("[event] session.idle - no hooks defined, skipping");
|
|
279
254
|
return;
|
|
280
255
|
}
|
package/dist/index.test.js
CHANGED
|
@@ -113,16 +113,39 @@ Content`;
|
|
|
113
113
|
expect(result["minimal"]).not.toHaveProperty("tools");
|
|
114
114
|
expect(result["minimal"]).not.toHaveProperty("permissions");
|
|
115
115
|
});
|
|
116
|
-
it("should
|
|
117
|
-
const
|
|
116
|
+
it("should use mode value directly (primary, subagent, all)", () => {
|
|
117
|
+
const primaryContent = `---
|
|
118
118
|
description: Primary agent
|
|
119
|
-
mode:
|
|
119
|
+
mode: primary
|
|
120
120
|
---
|
|
121
121
|
|
|
122
122
|
Content`;
|
|
123
|
-
|
|
123
|
+
const subagentContent = `---
|
|
124
|
+
description: Subagent
|
|
125
|
+
mode: subagent
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
Content`;
|
|
129
|
+
const allContent = `---
|
|
130
|
+
description: All modes agent
|
|
131
|
+
mode: all
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
Content`;
|
|
135
|
+
const noModeContent = `---
|
|
136
|
+
description: No mode specified
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
Content`;
|
|
140
|
+
writeFileSync(join(testDir, "primary.md"), primaryContent);
|
|
141
|
+
writeFileSync(join(testDir, "subagent.md"), subagentContent);
|
|
142
|
+
writeFileSync(join(testDir, "all.md"), allContent);
|
|
143
|
+
writeFileSync(join(testDir, "nomode.md"), noModeContent);
|
|
124
144
|
const result = loadAgents(testDir);
|
|
125
145
|
expect(result["primary"].mode).toBe("primary");
|
|
146
|
+
expect(result["subagent"].mode).toBe("subagent");
|
|
147
|
+
expect(result["all"].mode).toBe("all");
|
|
148
|
+
expect(result["nomode"].mode).toBe("all");
|
|
126
149
|
});
|
|
127
150
|
it("should ignore non-markdown files", () => {
|
|
128
151
|
writeFileSync(join(testDir, "not-agent.txt"), "some content");
|
|
@@ -286,7 +309,6 @@ hooks:
|
|
|
286
309
|
conditions: [isMainSession]
|
|
287
310
|
actions:
|
|
288
311
|
- command: simplify-changes
|
|
289
|
-
- skill: post-change-code-simplification
|
|
290
312
|
- tool:
|
|
291
313
|
name: bash
|
|
292
314
|
args:
|
|
@@ -296,10 +318,9 @@ hooks:
|
|
|
296
318
|
const result = loadHooks(testDir);
|
|
297
319
|
const hooks = result.get("session.idle");
|
|
298
320
|
expect(hooks).toHaveLength(1);
|
|
299
|
-
expect(hooks[0].actions).toHaveLength(
|
|
321
|
+
expect(hooks[0].actions).toHaveLength(2);
|
|
300
322
|
expect(hooks[0].actions[0]).toEqual({ command: "simplify-changes" });
|
|
301
|
-
expect(hooks[0].actions[1]).toEqual({
|
|
302
|
-
expect(hooks[0].actions[2]).toEqual({
|
|
323
|
+
expect(hooks[0].actions[1]).toEqual({
|
|
303
324
|
tool: { name: "bash", args: { command: "echo done" } }
|
|
304
325
|
});
|
|
305
326
|
});
|
package/dist/loaders.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export interface AgentFrontmatter {
|
|
2
2
|
description: string;
|
|
3
|
-
mode?: "subagent" | "
|
|
3
|
+
mode?: "primary" | "subagent" | "all";
|
|
4
|
+
model?: string;
|
|
4
5
|
temperature?: number;
|
|
6
|
+
maxSteps?: number;
|
|
7
|
+
disable?: boolean;
|
|
5
8
|
tools?: Record<string, boolean>;
|
|
6
9
|
permission?: Record<string, unknown>;
|
|
7
10
|
permissions?: Record<string, unknown>;
|
|
@@ -17,6 +20,7 @@ export interface CommandFrontmatter {
|
|
|
17
20
|
description: string;
|
|
18
21
|
agent?: string;
|
|
19
22
|
model?: string;
|
|
23
|
+
subtask?: boolean;
|
|
20
24
|
}
|
|
21
25
|
export interface CommandConfig {
|
|
22
26
|
template: string;
|
|
@@ -39,9 +43,6 @@ export interface HookActionCommand {
|
|
|
39
43
|
args: string;
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
|
-
export interface HookActionSkill {
|
|
43
|
-
skill: string;
|
|
44
|
-
}
|
|
45
46
|
export interface HookActionTool {
|
|
46
47
|
tool: {
|
|
47
48
|
name: string;
|
|
@@ -54,7 +55,7 @@ export interface HookActionBash {
|
|
|
54
55
|
timeout?: number;
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
|
-
export type HookAction = HookActionCommand |
|
|
58
|
+
export type HookAction = HookActionCommand | HookActionTool | HookActionBash;
|
|
58
59
|
export interface HookConfig {
|
|
59
60
|
event: HookEvent;
|
|
60
61
|
conditions?: HookCondition[];
|
|
@@ -63,7 +64,10 @@ export interface HookConfig {
|
|
|
63
64
|
export interface AgentConfigOutput {
|
|
64
65
|
description: string;
|
|
65
66
|
mode: "subagent" | "primary" | "all";
|
|
67
|
+
model?: string;
|
|
66
68
|
temperature?: number;
|
|
69
|
+
maxSteps?: number;
|
|
70
|
+
disable?: boolean;
|
|
67
71
|
tools?: Record<string, boolean>;
|
|
68
72
|
permissions?: Record<string, unknown>;
|
|
69
73
|
prompt: string;
|
package/dist/loaders.js
CHANGED
|
@@ -37,13 +37,16 @@ export function loadAgents(agentDir) {
|
|
|
37
37
|
const content = readFileSync(filePath, "utf-8");
|
|
38
38
|
const { data, body } = parseFrontmatter(content);
|
|
39
39
|
const agentName = basename(file, ".md");
|
|
40
|
-
const mode = data.mode
|
|
40
|
+
const mode = data.mode ?? "all";
|
|
41
41
|
const permissions = data.permissions ?? data.permission;
|
|
42
42
|
agents[agentName] = {
|
|
43
43
|
description: data.description || "",
|
|
44
44
|
mode,
|
|
45
45
|
prompt: body.trim(),
|
|
46
|
+
...(data.model !== undefined && { model: data.model }),
|
|
46
47
|
...(data.temperature !== undefined && { temperature: data.temperature }),
|
|
48
|
+
...(data.maxSteps !== undefined && { maxSteps: data.maxSteps }),
|
|
49
|
+
...(data.disable !== undefined && { disable: data.disable }),
|
|
47
50
|
...(data.tools !== undefined && { tools: data.tools }),
|
|
48
51
|
...(permissions !== undefined && { permissions }),
|
|
49
52
|
};
|
|
@@ -86,6 +89,7 @@ export function loadCommands(commandDir) {
|
|
|
86
89
|
description: data.description || "",
|
|
87
90
|
agent: data.agent,
|
|
88
91
|
model: data.model,
|
|
92
|
+
subtask: data.subtask,
|
|
89
93
|
template: body.trim(),
|
|
90
94
|
};
|
|
91
95
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to get Ethereum address balance
|
|
3
|
+
*/
|
|
4
|
+
import { type ToolContext } from "@opencode-ai/plugin";
|
|
5
|
+
export interface EthAddressBalanceArgs {
|
|
6
|
+
address: string;
|
|
7
|
+
chainId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function getAddressBalance(address: string, chainId?: string): Promise<string>;
|
|
10
|
+
export declare const ethAddressBalanceTool: {
|
|
11
|
+
description: string;
|
|
12
|
+
args: {
|
|
13
|
+
address: import("zod").ZodString;
|
|
14
|
+
chainId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
15
|
+
};
|
|
16
|
+
execute(args: {
|
|
17
|
+
address: string;
|
|
18
|
+
chainId?: string | undefined;
|
|
19
|
+
}, context: ToolContext): Promise<string>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to get Ethereum address balance
|
|
3
|
+
*/
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
import { EtherscanClient, EtherscanClientError, validateAddress } from "./etherscan-client";
|
|
6
|
+
import { formatBalance } from "./formatters";
|
|
7
|
+
import { CHAIN_ID_DESCRIPTION } from "./types";
|
|
8
|
+
export async function getAddressBalance(address, chainId) {
|
|
9
|
+
validateAddress(address);
|
|
10
|
+
const client = new EtherscanClient(undefined, chainId);
|
|
11
|
+
const balanceWei = await client.getBalance(address);
|
|
12
|
+
return formatBalance(address, balanceWei);
|
|
13
|
+
}
|
|
14
|
+
export const ethAddressBalanceTool = tool({
|
|
15
|
+
description: "Get the ETH balance of an Ethereum address. " +
|
|
16
|
+
"Returns balance in both ETH and Wei.",
|
|
17
|
+
args: {
|
|
18
|
+
address: tool.schema
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Ethereum address (0x...)"),
|
|
21
|
+
chainId: tool.schema
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(CHAIN_ID_DESCRIPTION),
|
|
25
|
+
},
|
|
26
|
+
async execute(args, _context) {
|
|
27
|
+
try {
|
|
28
|
+
return await getAddressBalance(args.address, args.chainId);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof EtherscanClientError) {
|
|
32
|
+
return `Error: ${error.message}`;
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to list Ethereum transactions for an address
|
|
3
|
+
*/
|
|
4
|
+
import { type ToolContext } from "@opencode-ai/plugin";
|
|
5
|
+
export interface EthAddressTxsArgs {
|
|
6
|
+
address: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
chainId?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getAddressTransactions(address: string, limit?: number, chainId?: string): Promise<string>;
|
|
11
|
+
export declare const ethAddressTxsTool: {
|
|
12
|
+
description: string;
|
|
13
|
+
args: {
|
|
14
|
+
address: import("zod").ZodString;
|
|
15
|
+
limit: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
16
|
+
chainId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
17
|
+
};
|
|
18
|
+
execute(args: {
|
|
19
|
+
address: string;
|
|
20
|
+
limit?: number | undefined;
|
|
21
|
+
chainId?: string | undefined;
|
|
22
|
+
}, context: ToolContext): Promise<string>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to list Ethereum transactions for an address
|
|
3
|
+
*/
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
import { EtherscanClient, EtherscanClientError, validateAddress } from "./etherscan-client";
|
|
6
|
+
import { formatTransactionList } from "./formatters";
|
|
7
|
+
import { DEFAULT_TRANSACTION_LIMIT, CHAIN_ID_DESCRIPTION } from "./types";
|
|
8
|
+
export async function getAddressTransactions(address, limit = DEFAULT_TRANSACTION_LIMIT, chainId) {
|
|
9
|
+
validateAddress(address);
|
|
10
|
+
const client = new EtherscanClient(undefined, chainId);
|
|
11
|
+
const transactions = await client.getTransactions(address, limit);
|
|
12
|
+
return formatTransactionList(address, transactions);
|
|
13
|
+
}
|
|
14
|
+
export const ethAddressTxsTool = tool({
|
|
15
|
+
description: "List Ethereum transactions for an address. " +
|
|
16
|
+
"Shows incoming and outgoing transactions with values, timestamps, and status.",
|
|
17
|
+
args: {
|
|
18
|
+
address: tool.schema
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Ethereum address (0x...)"),
|
|
21
|
+
limit: tool.schema
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(`Maximum number of transactions to return (default: ${DEFAULT_TRANSACTION_LIMIT})`),
|
|
25
|
+
chainId: tool.schema
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(CHAIN_ID_DESCRIPTION),
|
|
29
|
+
},
|
|
30
|
+
async execute(args, _context) {
|
|
31
|
+
try {
|
|
32
|
+
return await getAddressTransactions(args.address, args.limit, args.chainId);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof EtherscanClientError) {
|
|
36
|
+
return `Error: ${error.message}`;
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to list ERC-20 token transfers for an address
|
|
3
|
+
*/
|
|
4
|
+
import { type ToolContext } from "@opencode-ai/plugin";
|
|
5
|
+
export interface EthTokenTransfersArgs {
|
|
6
|
+
address: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
chainId?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getTokenTransfers(address: string, limit?: number, chainId?: string): Promise<string>;
|
|
11
|
+
export declare const ethTokenTransfersTool: {
|
|
12
|
+
description: string;
|
|
13
|
+
args: {
|
|
14
|
+
address: import("zod").ZodString;
|
|
15
|
+
limit: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
16
|
+
chainId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
17
|
+
};
|
|
18
|
+
execute(args: {
|
|
19
|
+
address: string;
|
|
20
|
+
limit?: number | undefined;
|
|
21
|
+
chainId?: string | undefined;
|
|
22
|
+
}, context: ToolContext): Promise<string>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to list ERC-20 token transfers for an address
|
|
3
|
+
*/
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
import { EtherscanClient, EtherscanClientError, validateAddress } from "./etherscan-client";
|
|
6
|
+
import { formatTokenTransferList } from "./formatters";
|
|
7
|
+
import { DEFAULT_TRANSACTION_LIMIT, CHAIN_ID_DESCRIPTION } from "./types";
|
|
8
|
+
export async function getTokenTransfers(address, limit = DEFAULT_TRANSACTION_LIMIT, chainId) {
|
|
9
|
+
validateAddress(address);
|
|
10
|
+
const client = new EtherscanClient(undefined, chainId);
|
|
11
|
+
const transfers = await client.getTokenTransfers(address, limit);
|
|
12
|
+
return formatTokenTransferList(address, transfers);
|
|
13
|
+
}
|
|
14
|
+
export const ethTokenTransfersTool = tool({
|
|
15
|
+
description: "List ERC-20 token transfers for an Ethereum address. " +
|
|
16
|
+
"Shows token names, symbols, values, and transaction details.",
|
|
17
|
+
args: {
|
|
18
|
+
address: tool.schema
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Ethereum address (0x...)"),
|
|
21
|
+
limit: tool.schema
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(`Maximum number of transfers to return (default: ${DEFAULT_TRANSACTION_LIMIT})`),
|
|
25
|
+
chainId: tool.schema
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(CHAIN_ID_DESCRIPTION),
|
|
29
|
+
},
|
|
30
|
+
async execute(args, _context) {
|
|
31
|
+
try {
|
|
32
|
+
return await getTokenTransfers(args.address, args.limit, args.chainId);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof EtherscanClientError) {
|
|
36
|
+
return `Error: ${error.message}`;
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to get Ethereum transaction details by hash
|
|
3
|
+
*/
|
|
4
|
+
import { type ToolContext } from "@opencode-ai/plugin";
|
|
5
|
+
export interface EthTransactionArgs {
|
|
6
|
+
hash: string;
|
|
7
|
+
chainId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function getTransactionDetails(hash: string, chainId?: string): Promise<string>;
|
|
10
|
+
export declare const ethTransactionTool: {
|
|
11
|
+
description: string;
|
|
12
|
+
args: {
|
|
13
|
+
hash: import("zod").ZodString;
|
|
14
|
+
chainId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
15
|
+
};
|
|
16
|
+
execute(args: {
|
|
17
|
+
hash: string;
|
|
18
|
+
chainId?: string | undefined;
|
|
19
|
+
}, context: ToolContext): Promise<string>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool to get Ethereum transaction details by hash
|
|
3
|
+
*/
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
import { EtherscanClient, EtherscanClientError, validateTxHash } from "./etherscan-client";
|
|
6
|
+
import { formatTransactionReceipt } from "./formatters";
|
|
7
|
+
import { CHAIN_ID_DESCRIPTION } from "./types";
|
|
8
|
+
export async function getTransactionDetails(hash, chainId) {
|
|
9
|
+
validateTxHash(hash);
|
|
10
|
+
const client = new EtherscanClient(undefined, chainId);
|
|
11
|
+
const receipt = await client.getTransactionReceipt(hash);
|
|
12
|
+
if (!receipt) {
|
|
13
|
+
return `Transaction not found: ${hash}`;
|
|
14
|
+
}
|
|
15
|
+
return formatTransactionReceipt(hash, receipt);
|
|
16
|
+
}
|
|
17
|
+
export const ethTransactionTool = tool({
|
|
18
|
+
description: "Get Ethereum transaction details by transaction hash. " +
|
|
19
|
+
"Returns status, block, addresses, gas costs, and log count.",
|
|
20
|
+
args: {
|
|
21
|
+
hash: tool.schema
|
|
22
|
+
.string()
|
|
23
|
+
.describe("Transaction hash (0x...)"),
|
|
24
|
+
chainId: tool.schema
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(CHAIN_ID_DESCRIPTION),
|
|
28
|
+
},
|
|
29
|
+
async execute(args, _context) {
|
|
30
|
+
try {
|
|
31
|
+
return await getTransactionDetails(args.hash, args.chainId);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof EtherscanClientError) {
|
|
35
|
+
return `Error: ${error.message}`;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Etherscan API client for Ethereum blockchain queries
|
|
3
|
+
*/
|
|
4
|
+
import { type EthTransaction, type EthTokenTransfer, type EthInternalTransaction } from "./types";
|
|
5
|
+
export declare class EtherscanClientError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class EtherscanClient {
|
|
9
|
+
private readonly apiKey;
|
|
10
|
+
private readonly chainId;
|
|
11
|
+
private readonly baseUrl;
|
|
12
|
+
constructor(apiKey?: string, chainId?: string, baseUrl?: string);
|
|
13
|
+
private request;
|
|
14
|
+
getBalance(address: string): Promise<string>;
|
|
15
|
+
getTransactions(address: string, limit?: number): Promise<EthTransaction[]>;
|
|
16
|
+
getInternalTransactions(address: string, limit?: number): Promise<EthInternalTransaction[]>;
|
|
17
|
+
getTokenTransfers(address: string, limit?: number): Promise<EthTokenTransfer[]>;
|
|
18
|
+
getTransactionByHash(hash: string): Promise<EthTransaction | null>;
|
|
19
|
+
getTransactionReceipt(hash: string): Promise<Record<string, unknown> | null>;
|
|
20
|
+
}
|
|
21
|
+
export declare function weiToEth(wei: string): string;
|
|
22
|
+
export declare function formatTimestamp(timestamp: string): string;
|
|
23
|
+
export declare function shortenAddress(address: string): string;
|
|
24
|
+
export declare function validateAddress(address: string): void;
|
|
25
|
+
export declare function validateTxHash(hash: string): void;
|