opencode-froggy 0.3.0 → 0.5.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 (35) hide show
  1. package/README.md +179 -59
  2. package/command/agent-promote.md +5 -0
  3. package/command/commit-push.md +8 -3
  4. package/command/diff-summary.md +51 -0
  5. package/command/doc-changes.md +1 -1
  6. package/command/gh-create-pr.md +18 -0
  7. package/command/review-changes.md +15 -1
  8. package/command/review-pr.md +4 -2
  9. package/command/simplify-changes.md +1 -1
  10. package/dist/index.js +11 -5
  11. package/dist/tools/agent-promote-core.d.ts +6 -0
  12. package/dist/tools/agent-promote-core.js +14 -0
  13. package/dist/tools/agent-promote.d.ts +19 -0
  14. package/dist/tools/agent-promote.js +39 -0
  15. package/dist/tools/agent-promote.test.d.ts +1 -0
  16. package/dist/tools/agent-promote.test.js +71 -0
  17. package/dist/tools/blockchain/eth-transaction.d.ts +15 -1
  18. package/dist/tools/blockchain/eth-transaction.js +180 -17
  19. package/dist/tools/blockchain/etherscan-client.d.ts +3 -2
  20. package/dist/tools/blockchain/etherscan-client.js +23 -5
  21. package/dist/tools/blockchain/event-decoder.d.ts +14 -0
  22. package/dist/tools/blockchain/event-decoder.js +96 -0
  23. package/dist/tools/blockchain/event-decoder.test.d.ts +1 -0
  24. package/dist/tools/blockchain/event-decoder.test.js +197 -0
  25. package/dist/tools/blockchain/types.d.ts +64 -0
  26. package/dist/tools/blockchain/viem-client.d.ts +9 -0
  27. package/dist/tools/blockchain/viem-client.js +98 -0
  28. package/dist/tools/index.d.ts +1 -1
  29. package/dist/tools/index.js +1 -1
  30. package/package.json +3 -2
  31. package/skill/code-simplify/SKILL.md +6 -0
  32. package/dist/tools/diff-summary.d.ts +0 -20
  33. package/dist/tools/diff-summary.js +0 -111
  34. package/dist/tools/reply-child.d.ts +0 -19
  35. package/dist/tools/reply-child.js +0 -42
@@ -85,6 +85,70 @@ export interface TransactionLog {
85
85
  logIndex: string;
86
86
  removed: boolean;
87
87
  }
88
+ export interface ContractInfo {
89
+ SourceCode: string;
90
+ ABI: string;
91
+ ContractName: string;
92
+ CompilerVersion: string;
93
+ OptimizationUsed: string;
94
+ Runs: string;
95
+ ConstructorArguments: string;
96
+ EVMVersion: string;
97
+ Library: string;
98
+ LicenseType: string;
99
+ Proxy: string;
100
+ Implementation: string;
101
+ SwarmSource: string;
102
+ }
103
+ export interface LabeledAddress {
104
+ address: string;
105
+ label: string | null;
106
+ }
107
+ export interface TokenMetadata {
108
+ name: string;
109
+ symbol: string;
110
+ decimals: number;
111
+ }
112
+ export interface DecodedEvent {
113
+ name: string;
114
+ address: LabeledAddress;
115
+ params: Record<string, string>;
116
+ }
117
+ export interface TransactionDetails {
118
+ hash: string;
119
+ status: "success" | "failed";
120
+ block: number;
121
+ timestamp: string | null;
122
+ from: LabeledAddress;
123
+ to: LabeledAddress | null;
124
+ value: string;
125
+ gas: {
126
+ used: number;
127
+ price: string;
128
+ cost: string;
129
+ };
130
+ internalTransactions?: InternalTransactionDetails[];
131
+ tokenTransfers?: TokenTransferDetails[];
132
+ decodedEvents?: DecodedEvent[];
133
+ undecodedEventsCount?: number;
134
+ }
135
+ export interface InternalTransactionDetails {
136
+ from: LabeledAddress;
137
+ to: LabeledAddress;
138
+ value: string;
139
+ type: string;
140
+ }
141
+ export interface TokenTransferDetails {
142
+ token: {
143
+ address: string;
144
+ name: string;
145
+ symbol: string;
146
+ decimals: number;
147
+ };
148
+ from: LabeledAddress;
149
+ to: LabeledAddress;
150
+ value: string;
151
+ }
88
152
  export declare const DEFAULT_TRANSACTION_LIMIT = 20;
89
153
  export declare const DEFAULT_CHAIN_ID = "1";
90
154
  export declare const CHAIN_ID_DESCRIPTION: string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Viem client for EVM RPC calls
3
+ * Uses public RPCs to avoid Etherscan rate limits
4
+ */
5
+ import { type TransactionReceipt, type Block } from "viem";
6
+ import { type TokenMetadata } from "./types";
7
+ export declare function getTransactionReceipt(hash: string, chainId?: string): Promise<TransactionReceipt | null>;
8
+ export declare function getBlock(blockNumber: bigint, chainId?: string): Promise<Block | null>;
9
+ export declare function getTokenMetadata(contractAddress: string, chainId?: string): Promise<TokenMetadata>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Viem client for EVM RPC calls
3
+ * Uses public RPCs to avoid Etherscan rate limits
4
+ */
5
+ import { createPublicClient, http, parseAbi, } from "viem";
6
+ import { mainnet, polygon, arbitrum, optimism, base, bsc, avalanche, fantom, zkSync, gnosis, celo, linea, scroll, mantle, blast, } from "viem/chains";
7
+ import { DEFAULT_CHAIN_ID } from "./types";
8
+ const CHAIN_MAP = {
9
+ "1": mainnet,
10
+ "137": polygon,
11
+ "42161": arbitrum,
12
+ "10": optimism,
13
+ "8453": base,
14
+ "56": bsc,
15
+ "43114": avalanche,
16
+ "250": fantom,
17
+ "324": zkSync,
18
+ "100": gnosis,
19
+ "42220": celo,
20
+ "59144": linea,
21
+ "534352": scroll,
22
+ "5000": mantle,
23
+ "81457": blast,
24
+ };
25
+ const ERC20_ABI = parseAbi([
26
+ "function name() view returns (string)",
27
+ "function symbol() view returns (string)",
28
+ "function decimals() view returns (uint8)",
29
+ ]);
30
+ const clientCache = new Map();
31
+ function getClient(chainId) {
32
+ const cached = clientCache.get(chainId);
33
+ if (cached) {
34
+ return cached;
35
+ }
36
+ const chain = CHAIN_MAP[chainId] ?? mainnet;
37
+ const client = createPublicClient({
38
+ chain,
39
+ transport: http(),
40
+ batch: {
41
+ multicall: true,
42
+ },
43
+ });
44
+ clientCache.set(chainId, client);
45
+ return client;
46
+ }
47
+ export async function getTransactionReceipt(hash, chainId = DEFAULT_CHAIN_ID) {
48
+ const client = getClient(chainId);
49
+ try {
50
+ return await client.getTransactionReceipt({
51
+ hash: hash,
52
+ });
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ export async function getBlock(blockNumber, chainId = DEFAULT_CHAIN_ID) {
59
+ const client = getClient(chainId);
60
+ try {
61
+ return await client.getBlock({ blockNumber });
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ export async function getTokenMetadata(contractAddress, chainId = DEFAULT_CHAIN_ID) {
68
+ const client = getClient(chainId);
69
+ const address = contractAddress;
70
+ const [name, symbol, decimals] = await Promise.all([
71
+ client
72
+ .readContract({
73
+ address,
74
+ abi: ERC20_ABI,
75
+ functionName: "name",
76
+ })
77
+ .catch(() => null),
78
+ client
79
+ .readContract({
80
+ address,
81
+ abi: ERC20_ABI,
82
+ functionName: "symbol",
83
+ })
84
+ .catch(() => null),
85
+ client
86
+ .readContract({
87
+ address,
88
+ abi: ERC20_ABI,
89
+ functionName: "decimals",
90
+ })
91
+ .catch(() => null),
92
+ ]);
93
+ return {
94
+ name: name ?? "Unknown",
95
+ symbol: symbol ?? "???",
96
+ decimals: decimals ?? 18,
97
+ };
98
+ }
@@ -1,5 +1,5 @@
1
1
  export { gitingestTool, fetchGitingest, type GitingestArgs } from "./gitingest";
2
- export { createDiffSummaryTool, diffSummary, type DiffSummaryArgs } from "./diff-summary";
3
2
  export { createPromptSessionTool, type PromptSessionArgs } from "./prompt-session";
4
3
  export { createListChildSessionsTool } from "./list-child-sessions";
4
+ export { createAgentPromoteTool, getPromotedAgents, type AgentPromoteArgs } from "./agent-promote";
5
5
  export { ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, EtherscanClient, EtherscanClientError, weiToEth, formatTimestamp, shortenAddress, type EthTransactionArgs, type EthAddressTxsArgs, type EthAddressBalanceArgs, type EthTokenTransfersArgs, } from "./blockchain";
@@ -1,5 +1,5 @@
1
1
  export { gitingestTool, fetchGitingest } from "./gitingest";
2
- export { createDiffSummaryTool, diffSummary } from "./diff-summary";
3
2
  export { createPromptSessionTool } from "./prompt-session";
4
3
  export { createListChildSessionsTool } from "./list-child-sessions";
4
+ export { createAgentPromoteTool, getPromotedAgents } from "./agent-promote";
5
5
  export { ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, EtherscanClient, EtherscanClientError, weiToEth, formatTimestamp, shortenAddress, } from "./blockchain";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-froggy",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenCode plugin with a hook layer (tool.before.*, session.idle...), agents (code-reviewer, doc-writer), and commands (/review-pr, /commit)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,7 +38,8 @@
38
38
  "url": "https://github.com/smartfrog/opencode-froggy"
39
39
  },
40
40
  "dependencies": {
41
- "js-yaml": "^4.1.0"
41
+ "js-yaml": "^4.1.0",
42
+ "viem": "^2.44.1"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@opencode-ai/plugin": "latest",
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: code-simplify
3
+ description: Simplify code you just wrote or modified. Load after completing a feature, fix, or refactor to improve clarity while preserving behavior.
4
+ ---
5
+
6
+ Run the `/simplify-changes` command.
@@ -1,20 +0,0 @@
1
- import { type ToolContext } from "@opencode-ai/plugin";
2
- export interface DiffSummaryArgs {
3
- source?: string;
4
- target?: string;
5
- remote?: string;
6
- }
7
- export declare function diffSummary(args: DiffSummaryArgs, cwd: string): Promise<string>;
8
- export declare function createDiffSummaryTool(directory: string): {
9
- description: string;
10
- args: {
11
- source: import("zod").ZodOptional<import("zod").ZodString>;
12
- target: import("zod").ZodOptional<import("zod").ZodString>;
13
- remote: import("zod").ZodOptional<import("zod").ZodString>;
14
- };
15
- execute(args: {
16
- source?: string | undefined;
17
- target?: string | undefined;
18
- remote?: string | undefined;
19
- }, context: ToolContext): Promise<string>;
20
- };
@@ -1,111 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { tool } from "@opencode-ai/plugin";
4
- import { log } from "../logger";
5
- const execFileAsync = promisify(execFile);
6
- const DIFF_CONTEXT_LINES = 5;
7
- async function git(args, cwd) {
8
- try {
9
- const result = await execFileAsync("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
10
- return result.stdout;
11
- }
12
- catch (error) {
13
- const execError = error;
14
- if (execError.stdout)
15
- return execError.stdout;
16
- throw error;
17
- }
18
- }
19
- async function getDiffSet(cwd, extraArgs = []) {
20
- const [stats, files, diff] = await Promise.all([
21
- git(["diff", ...extraArgs, "--stat"], cwd),
22
- git(["diff", ...extraArgs, "--name-status"], cwd),
23
- git(["diff", ...extraArgs, `-U${DIFF_CONTEXT_LINES}`, "--function-context"], cwd),
24
- ]);
25
- return { stats, files, diff };
26
- }
27
- async function getBranchesDiff(source, target, remote, cwd) {
28
- const refSource = remote ? `${remote}/${source}` : source;
29
- const refTarget = remote ? `${remote}/${target}` : target;
30
- const range = `${refTarget}...${refSource}`;
31
- const rangeLog = `${refTarget}..${refSource}`;
32
- if (remote) {
33
- await git(["fetch", remote, source, target, "--prune"], cwd);
34
- }
35
- const [stats, commits, files, diff] = await Promise.all([
36
- git(["diff", "--stat", range], cwd),
37
- git(["log", "--oneline", "--no-merges", rangeLog], cwd),
38
- git(["diff", "--name-only", range], cwd),
39
- git(["diff", `-U${DIFF_CONTEXT_LINES}`, "--function-context", range], cwd),
40
- ]);
41
- return [
42
- "## Stats Overview", "```", stats.trim(), "```",
43
- "",
44
- "## Commits to Review", "```", commits.trim(), "```",
45
- "",
46
- "## Files Changed", "```", files.trim(), "```",
47
- "",
48
- "## Full Diff", "```diff", diff.trim(), "```",
49
- ].join("\n\n");
50
- }
51
- async function getWorkingTreeDiff(cwd) {
52
- const sections = [];
53
- const [status, staged, unstaged, untrackedList] = await Promise.all([
54
- git(["status", "--porcelain=v1", "-uall"], cwd),
55
- getDiffSet(cwd, ["--cached"]),
56
- getDiffSet(cwd),
57
- git(["ls-files", "--others", "--exclude-standard"], cwd),
58
- ]);
59
- sections.push("## Status Overview", "```", status.trim() || "(no changes)", "```");
60
- if (staged.stats.trim() || staged.files.trim()) {
61
- sections.push("## Staged Changes", "### Stats", "```", staged.stats.trim() || "(none)", "```", "### Files", "```", staged.files.trim() || "(none)", "```", "### Diff", "```diff", staged.diff.trim() || "(none)", "```");
62
- }
63
- if (unstaged.stats.trim() || unstaged.files.trim()) {
64
- sections.push("## Unstaged Changes", "### Stats", "```", unstaged.stats.trim() || "(none)", "```", "### Files", "```", unstaged.files.trim() || "(none)", "```", "### Diff", "```diff", unstaged.diff.trim() || "(none)", "```");
65
- }
66
- const untrackedFiles = untrackedList.trim().split("\n").filter(Boolean);
67
- if (untrackedFiles.length > 0) {
68
- const untrackedDiffs = [];
69
- for (const file of untrackedFiles) {
70
- try {
71
- const fileDiff = await git(["diff", "--no-index", `-U${DIFF_CONTEXT_LINES}`, "--function-context", "--", "/dev/null", file], cwd);
72
- untrackedDiffs.push(`=== NEW: ${file} ===\n${fileDiff.trim()}`);
73
- }
74
- catch (error) {
75
- log("[diff-summary] failed to diff untracked file", { file, error: String(error) });
76
- untrackedDiffs.push(`=== NEW: ${file} === (could not diff)`);
77
- }
78
- }
79
- sections.push("## Untracked Files", "```", untrackedFiles.join("\n"), "```", "### Diffs", "```diff", untrackedDiffs.join("\n\n"), "```");
80
- }
81
- return sections.join("\n\n");
82
- }
83
- export async function diffSummary(args, cwd) {
84
- const { source, target = "main", remote = "origin" } = args;
85
- if (source) {
86
- return getBranchesDiff(source, target, remote, cwd);
87
- }
88
- return getWorkingTreeDiff(cwd);
89
- }
90
- export function createDiffSummaryTool(directory) {
91
- return tool({
92
- description: "Generate a structured summary of git diffs. Use for reviewing branches comparison or working tree changes. Returns stats, commits, files changed, and full diff.",
93
- args: {
94
- source: tool.schema
95
- .string()
96
- .optional()
97
- .describe("Source branch to compare (e.g., 'feature-branch'). If omitted, analyzes working tree changes."),
98
- target: tool.schema
99
- .string()
100
- .optional()
101
- .describe("Target branch to compare against (default: 'main')"),
102
- remote: tool.schema
103
- .string()
104
- .optional()
105
- .describe("Git remote name (default: 'origin')"),
106
- },
107
- async execute(args, _context) {
108
- return diffSummary(args, directory);
109
- },
110
- });
111
- }
@@ -1,19 +0,0 @@
1
- import { type ToolContext } from "@opencode-ai/plugin";
2
- import type { createOpencodeClient } from "@opencode-ai/sdk";
3
- type Client = ReturnType<typeof createOpencodeClient>;
4
- export interface ReplyChildArgs {
5
- message: string;
6
- sessionId?: string;
7
- }
8
- export declare function createReplyChildTool(client: Client): {
9
- description: string;
10
- args: {
11
- message: import("zod").ZodString;
12
- sessionId: import("zod").ZodOptional<import("zod").ZodString>;
13
- };
14
- execute(args: {
15
- message: string;
16
- sessionId?: string | undefined;
17
- }, context: ToolContext): Promise<string>;
18
- };
19
- export {};
@@ -1,42 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { log } from "../logger";
3
- export function createReplyChildTool(client) {
4
- return tool({
5
- description: "Send a message to the last child session (subagent) to continue the conversation",
6
- args: {
7
- message: tool.schema.string().describe("The message to send to the child session"),
8
- sessionId: tool.schema.string().optional().describe("The child session ID to target (optional - uses last child if not provided)"),
9
- },
10
- async execute(args, context) {
11
- let targetSessionId = args.sessionId;
12
- if (!targetSessionId) {
13
- const children = await client.session.children({
14
- path: { id: context.sessionID },
15
- });
16
- const lastChild = (children.data ?? []).at(-1);
17
- if (!lastChild) {
18
- return "Error: No child session found for current session";
19
- }
20
- targetSessionId = lastChild.id;
21
- }
22
- log("[reply-child] Sending message to child session", {
23
- parentSessionID: context.sessionID,
24
- childSessionID: targetSessionId,
25
- message: args.message.slice(0, 100),
26
- });
27
- const response = await client.session.prompt({
28
- path: { id: targetSessionId },
29
- body: { parts: [{ type: "text", text: args.message }] },
30
- });
31
- log("[reply-child] Response received", {
32
- childSessionID: targetSessionId,
33
- });
34
- const parts = response.data?.parts ?? [];
35
- const textContent = parts
36
- .filter((p) => p.type === "text" && p.text)
37
- .map((p) => p.text)
38
- .join("\n");
39
- return textContent || "Message sent to child session";
40
- },
41
- });
42
- }