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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +508 -246
  3. package/agent/architect.md +91 -0
  4. package/agent/partner.md +143 -0
  5. package/agent/rubber-duck.md +129 -0
  6. package/command/commit-push.md +21 -0
  7. package/command/doc-changes.md +45 -0
  8. package/command/review-changes.md +1 -21
  9. package/command/review-pr.md +1 -22
  10. package/command/send-to.md +21 -0
  11. package/command/simplify-changes.md +2 -20
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +27 -52
  14. package/dist/index.test.js +29 -8
  15. package/dist/loaders.d.ts +9 -5
  16. package/dist/loaders.js +5 -1
  17. package/dist/tools/blockchain/eth-address-balance.d.ts +20 -0
  18. package/dist/tools/blockchain/eth-address-balance.js +37 -0
  19. package/dist/tools/blockchain/eth-address-txs.d.ts +23 -0
  20. package/dist/tools/blockchain/eth-address-txs.js +41 -0
  21. package/dist/tools/blockchain/eth-token-transfers.d.ts +23 -0
  22. package/dist/tools/blockchain/eth-token-transfers.js +41 -0
  23. package/dist/tools/blockchain/eth-transaction.d.ts +20 -0
  24. package/dist/tools/blockchain/eth-transaction.js +40 -0
  25. package/dist/tools/blockchain/etherscan-client.d.ts +25 -0
  26. package/dist/tools/blockchain/etherscan-client.js +156 -0
  27. package/dist/tools/blockchain/etherscan-client.test.d.ts +1 -0
  28. package/dist/tools/blockchain/etherscan-client.test.js +211 -0
  29. package/dist/tools/blockchain/formatters.d.ts +10 -0
  30. package/dist/tools/blockchain/formatters.js +147 -0
  31. package/dist/tools/blockchain/index.d.ts +10 -0
  32. package/dist/tools/blockchain/index.js +10 -0
  33. package/dist/tools/blockchain/tools.test.d.ts +1 -0
  34. package/dist/tools/blockchain/tools.test.js +208 -0
  35. package/dist/tools/blockchain/types.d.ts +90 -0
  36. package/dist/tools/blockchain/types.js +8 -0
  37. package/dist/tools/diff-summary.d.ts +20 -0
  38. package/dist/tools/diff-summary.js +111 -0
  39. package/dist/tools/gitingest.d.ts +26 -0
  40. package/dist/tools/gitingest.js +41 -0
  41. package/dist/tools/index.d.ts +5 -0
  42. package/dist/tools/index.js +5 -0
  43. package/dist/tools/list-child-sessions.d.ts +9 -0
  44. package/dist/tools/list-child-sessions.js +24 -0
  45. package/dist/tools/prompt-session.d.ts +19 -0
  46. package/dist/tools/prompt-session.js +39 -0
  47. package/dist/tools/reply-child.d.ts +19 -0
  48. package/dist/tools/reply-child.js +42 -0
  49. package/images/logo.png +0 -0
  50. package/package.json +4 -2
  51. 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, loadSkills, loadCommands, loadHooks, mergeHooks, } from "./loaders";
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
- export { parseFrontmatter, loadAgents, loadSkills, loadCommands } from "./loaders";
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" && sessionID !== mainSessionID) {
44
- log(`${prefix} condition not met, skipping`, { sessionID, condition });
45
- return { blocked: false };
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
- skill: tool({
188
- description: `Load a skill to get detailed instructions for a specific task. Skills provide specialized knowledge and step-by-step guidance. Use this when a task matches an available skill's description. <available_skills>${skills.map((s) => `\n <skill>\n <name>${s.name}</name>\n <description>${s.description}</description>\n </skill>`).join("")}\n</available_skills>`,
189
- args: {
190
- name: tool.schema
191
- .string()
192
- .describe("The skill identifier from available_skills (e.g., 'post-change-code-simplification')"),
193
- },
194
- async execute(args, _context) {
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
- if (!mainSessionID) {
273
- mainSessionID = sessionID;
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
  }
@@ -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 convert agent mode to primary", () => {
117
- const agentContent = `---
116
+ it("should use mode value directly (primary, subagent, all)", () => {
117
+ const primaryContent = `---
118
118
  description: Primary agent
119
- mode: agent
119
+ mode: primary
120
120
  ---
121
121
 
122
122
  Content`;
123
- writeFileSync(join(testDir, "primary.md"), agentContent);
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(3);
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({ skill: "post-change-code-simplification" });
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" | "agent";
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 | HookActionSkill | HookActionTool | HookActionBash;
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 === "agent" ? "primary" : "subagent";
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;