opencode-froggy 0.3.0 → 0.4.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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Event decoder for common ERC-20 and WETH events
3
+ */
4
+ const EVENT_SIGNATURES = {
5
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": "Transfer",
6
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": "Approval",
7
+ "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c": "Deposit",
8
+ "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65": "Withdrawal",
9
+ };
10
+ function decodeAddress(topic) {
11
+ return "0x" + topic.slice(26).toLowerCase();
12
+ }
13
+ function decodeUint256(data) {
14
+ const hex = data.startsWith("0x") ? data.slice(2) : data;
15
+ if (hex === "")
16
+ return "0";
17
+ return BigInt("0x" + hex).toString();
18
+ }
19
+ export async function decodeEvent(log, resolver) {
20
+ const topic0 = log.topics[0];
21
+ const signature = EVENT_SIGNATURES[topic0];
22
+ if (!signature) {
23
+ return null;
24
+ }
25
+ const contractAddress = await resolver.resolve(log.address);
26
+ switch (signature) {
27
+ case "Transfer": {
28
+ if (log.topics.length < 3)
29
+ return null;
30
+ const from = decodeAddress(log.topics[1]);
31
+ const to = decodeAddress(log.topics[2]);
32
+ const value = decodeUint256(log.data);
33
+ return {
34
+ name: "Transfer",
35
+ address: contractAddress,
36
+ params: { from, to, value },
37
+ };
38
+ }
39
+ case "Approval": {
40
+ if (log.topics.length < 3)
41
+ return null;
42
+ const owner = decodeAddress(log.topics[1]);
43
+ const spender = decodeAddress(log.topics[2]);
44
+ const value = decodeUint256(log.data);
45
+ return {
46
+ name: "Approval",
47
+ address: contractAddress,
48
+ params: { owner, spender, value },
49
+ };
50
+ }
51
+ case "Deposit": {
52
+ if (log.topics.length < 2)
53
+ return null;
54
+ const dst = decodeAddress(log.topics[1]);
55
+ const wad = decodeUint256(log.data);
56
+ return {
57
+ name: "Deposit",
58
+ address: contractAddress,
59
+ params: { dst, wad },
60
+ };
61
+ }
62
+ case "Withdrawal": {
63
+ if (log.topics.length < 2)
64
+ return null;
65
+ const src = decodeAddress(log.topics[1]);
66
+ const wad = decodeUint256(log.data);
67
+ return {
68
+ name: "Withdrawal",
69
+ address: contractAddress,
70
+ params: { src, wad },
71
+ };
72
+ }
73
+ default:
74
+ return null;
75
+ }
76
+ }
77
+ export async function decodeEvents(logs, resolver) {
78
+ const decoded = [];
79
+ let undecodedCount = 0;
80
+ for (const log of logs) {
81
+ const event = await decodeEvent(log, resolver);
82
+ if (event) {
83
+ decoded.push(event);
84
+ }
85
+ else {
86
+ undecodedCount++;
87
+ }
88
+ }
89
+ return { decoded, undecodedCount };
90
+ }
91
+ export function isKnownEvent(topic0) {
92
+ return topic0 in EVENT_SIGNATURES;
93
+ }
94
+ export function getEventName(topic0) {
95
+ return EVENT_SIGNATURES[topic0] ?? null;
96
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { decodeEvent, decodeEvents, isKnownEvent, getEventName, } from "./event-decoder";
3
+ const mockResolver = {
4
+ async resolve(address) {
5
+ return { address, label: null };
6
+ },
7
+ };
8
+ describe("Event Decoder", () => {
9
+ describe("isKnownEvent", () => {
10
+ it("should recognize Transfer event", () => {
11
+ const topic0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
12
+ expect(isKnownEvent(topic0)).toBe(true);
13
+ });
14
+ it("should recognize Approval event", () => {
15
+ const topic0 = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925";
16
+ expect(isKnownEvent(topic0)).toBe(true);
17
+ });
18
+ it("should recognize Deposit event", () => {
19
+ const topic0 = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
20
+ expect(isKnownEvent(topic0)).toBe(true);
21
+ });
22
+ it("should recognize Withdrawal event", () => {
23
+ const topic0 = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
24
+ expect(isKnownEvent(topic0)).toBe(true);
25
+ });
26
+ it("should return false for unknown event", () => {
27
+ const topic0 = "0x0000000000000000000000000000000000000000000000000000000000000000";
28
+ expect(isKnownEvent(topic0)).toBe(false);
29
+ });
30
+ });
31
+ describe("getEventName", () => {
32
+ it("should return Transfer for Transfer topic", () => {
33
+ const topic0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
34
+ expect(getEventName(topic0)).toBe("Transfer");
35
+ });
36
+ it("should return null for unknown topic", () => {
37
+ const topic0 = "0x0000000000000000000000000000000000000000000000000000000000000000";
38
+ expect(getEventName(topic0)).toBe(null);
39
+ });
40
+ });
41
+ describe("decodeEvent", () => {
42
+ it("should decode Transfer event", async () => {
43
+ const log = {
44
+ address: "0x6b175474e89094c44da98b954eedeac495271d0f",
45
+ topics: [
46
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
47
+ "0x000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8",
48
+ "0x000000000000000000000000000000002b0184b12e908bf97941f2f5385c9820",
49
+ ],
50
+ data: "0x000000000000000000000000000000000000000000000278fd3c000000000000",
51
+ blockNumber: "0x1234",
52
+ transactionHash: "0xabc",
53
+ transactionIndex: "0x0",
54
+ blockHash: "0xdef",
55
+ logIndex: "0x0",
56
+ removed: false,
57
+ };
58
+ const result = await decodeEvent(log, mockResolver);
59
+ expect(result).not.toBeNull();
60
+ expect(result.name).toBe("Transfer");
61
+ expect(result.params.from).toBe("0xba12222222228d8ba445958a75a0704d566bf2c8");
62
+ expect(result.params.to).toBe("0x000000002b0184b12e908bf97941f2f5385c9820");
63
+ expect(result.params.value).toBe("11676589714374635028480");
64
+ });
65
+ it("should decode Approval event", async () => {
66
+ const log = {
67
+ address: "0x6b175474e89094c44da98b954eedeac495271d0f",
68
+ topics: [
69
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
70
+ "0x0000000000000000000000001111111111111111111111111111111111111111",
71
+ "0x0000000000000000000000002222222222222222222222222222222222222222",
72
+ ],
73
+ data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
74
+ blockNumber: "0x1234",
75
+ transactionHash: "0xabc",
76
+ transactionIndex: "0x0",
77
+ blockHash: "0xdef",
78
+ logIndex: "0x0",
79
+ removed: false,
80
+ };
81
+ const result = await decodeEvent(log, mockResolver);
82
+ expect(result).not.toBeNull();
83
+ expect(result.name).toBe("Approval");
84
+ expect(result.params.owner).toBe("0x1111111111111111111111111111111111111111");
85
+ expect(result.params.spender).toBe("0x2222222222222222222222222222222222222222");
86
+ expect(result.params.value).toBe("1000000000000000000");
87
+ });
88
+ it("should decode Deposit event (WETH)", async () => {
89
+ const log = {
90
+ address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
91
+ topics: [
92
+ "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c",
93
+ "0x0000000000000000000000001111111111111111111111111111111111111111",
94
+ ],
95
+ data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
96
+ blockNumber: "0x1234",
97
+ transactionHash: "0xabc",
98
+ transactionIndex: "0x0",
99
+ blockHash: "0xdef",
100
+ logIndex: "0x0",
101
+ removed: false,
102
+ };
103
+ const result = await decodeEvent(log, mockResolver);
104
+ expect(result).not.toBeNull();
105
+ expect(result.name).toBe("Deposit");
106
+ expect(result.params.dst).toBe("0x1111111111111111111111111111111111111111");
107
+ expect(result.params.wad).toBe("1000000000000000000");
108
+ });
109
+ it("should decode Withdrawal event (WETH)", async () => {
110
+ const log = {
111
+ address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
112
+ topics: [
113
+ "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
114
+ "0x0000000000000000000000001111111111111111111111111111111111111111",
115
+ ],
116
+ data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
117
+ blockNumber: "0x1234",
118
+ transactionHash: "0xabc",
119
+ transactionIndex: "0x0",
120
+ blockHash: "0xdef",
121
+ logIndex: "0x0",
122
+ removed: false,
123
+ };
124
+ const result = await decodeEvent(log, mockResolver);
125
+ expect(result).not.toBeNull();
126
+ expect(result.name).toBe("Withdrawal");
127
+ expect(result.params.src).toBe("0x1111111111111111111111111111111111111111");
128
+ expect(result.params.wad).toBe("1000000000000000000");
129
+ });
130
+ it("should return null for unknown event", async () => {
131
+ const log = {
132
+ address: "0x1234567890123456789012345678901234567890",
133
+ topics: ["0x0000000000000000000000000000000000000000000000000000000000000000"],
134
+ data: "0x",
135
+ blockNumber: "0x1234",
136
+ transactionHash: "0xabc",
137
+ transactionIndex: "0x0",
138
+ blockHash: "0xdef",
139
+ logIndex: "0x0",
140
+ removed: false,
141
+ };
142
+ const result = await decodeEvent(log, mockResolver);
143
+ expect(result).toBeNull();
144
+ });
145
+ });
146
+ describe("decodeEvents", () => {
147
+ it("should decode multiple events and count undecoded", async () => {
148
+ const logs = [
149
+ {
150
+ address: "0x6b175474e89094c44da98b954eedeac495271d0f",
151
+ topics: [
152
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
153
+ "0x0000000000000000000000001111111111111111111111111111111111111111",
154
+ "0x0000000000000000000000002222222222222222222222222222222222222222",
155
+ ],
156
+ data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
157
+ blockNumber: "0x1234",
158
+ transactionHash: "0xabc",
159
+ transactionIndex: "0x0",
160
+ blockHash: "0xdef",
161
+ logIndex: "0x0",
162
+ removed: false,
163
+ },
164
+ {
165
+ address: "0x1234567890123456789012345678901234567890",
166
+ topics: ["0x0000000000000000000000000000000000000000000000000000000000000000"],
167
+ data: "0x",
168
+ blockNumber: "0x1234",
169
+ transactionHash: "0xabc",
170
+ transactionIndex: "0x0",
171
+ blockHash: "0xdef",
172
+ logIndex: "0x1",
173
+ removed: false,
174
+ },
175
+ {
176
+ address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
177
+ topics: [
178
+ "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c",
179
+ "0x0000000000000000000000001111111111111111111111111111111111111111",
180
+ ],
181
+ data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
182
+ blockNumber: "0x1234",
183
+ transactionHash: "0xabc",
184
+ transactionIndex: "0x0",
185
+ blockHash: "0xdef",
186
+ logIndex: "0x2",
187
+ removed: false,
188
+ },
189
+ ];
190
+ const result = await decodeEvents(logs, mockResolver);
191
+ expect(result.decoded.length).toBe(2);
192
+ expect(result.undecodedCount).toBe(1);
193
+ expect(result.decoded[0].name).toBe("Transfer");
194
+ expect(result.decoded[1].name).toBe("Deposit");
195
+ });
196
+ });
197
+ });
@@ -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,4 @@
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";
5
4
  export { ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, EtherscanClient, EtherscanClientError, weiToEth, formatTimestamp, shortenAddress, type EthTransactionArgs, type EthAddressTxsArgs, type EthAddressBalanceArgs, type EthTokenTransfersArgs, } from "./blockchain";
@@ -1,5 +1,4 @@
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";
5
4
  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.4.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 {};