opencode-froggy 0.4.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.
package/README.md CHANGED
@@ -18,9 +18,9 @@ Plugin providing Claude Code–style hooks, specialized agents (doc-writer, code
18
18
  - [Agents](#agents)
19
19
  - [Tools](#tools)
20
20
  - [gitingest](#gitingest)
21
- - [diff-summary](#diff-summary)
22
21
  - [prompt-session](#prompt-session)
23
22
  - [list-child-sessions](#list-child-sessions)
23
+ - [agent-promote](#agent-promote)
24
24
  - [Blockchain](#blockchain)
25
25
  - [Configuration](#configuration)
26
26
  - [eth-transaction](#eth-transaction)
@@ -66,7 +66,9 @@ Alternatively, clone or copy the plugin files to one of these directories:
66
66
 
67
67
  | Command | Description | Agent |
68
68
  |---------|-------------|-------|
69
+ | `/agent-promote <name> <grade>` | Change the type of a plugin agent at runtime. Grades: `subagent`, `primary`, `all` | - |
69
70
  | `/commit-push` | Stage, commit, and push changes with user confirmation | `build` |
71
+ | `/diff-summary [source] [target]` | Show working tree changes or diff between branches | - |
70
72
  | `/doc-changes` | Update documentation based on uncommitted changes (new features only) | `doc-writer` |
71
73
  | `/review-changes` | Review uncommitted changes (staged + unstaged, including untracked files) | `code-reviewer` |
72
74
  | `/review-pr <source> <target>` | Review changes from source branch into target branch | `code-reviewer` |
@@ -74,6 +76,28 @@ Alternatively, clone or copy the plugin files to one of these directories:
74
76
  | `/simplify-changes` | Simplify uncommitted changes (staged + unstaged, including untracked files) | `code-simplifier` |
75
77
  | `/tests-coverage` | Run the full test suite with coverage report and suggest fixes for failures | `build` |
76
78
 
79
+ ### /diff-summary
80
+
81
+ The `/diff-summary` command supports two modes:
82
+
83
+ **Working tree mode** (no parameters):
84
+ ```bash
85
+ /diff-summary
86
+ ```
87
+ Shows staged changes, unstaged changes, and untracked file contents.
88
+
89
+ **Branch comparison mode** (with parameters):
90
+ ```bash
91
+ # Compare a branch with the current branch (HEAD)
92
+ /diff-summary feature-branch
93
+
94
+ # Compare two specific branches
95
+ /diff-summary feature-branch main
96
+ ```
97
+ Shows stats overview, commits, files changed, and full diff between branches.
98
+
99
+ > **Note:** The `/review-pr` command uses `/diff-summary` internally to generate the diff for code review.
100
+
77
101
  ---
78
102
 
79
103
  ## Agents
@@ -139,50 +163,6 @@ gitingest({
139
163
 
140
164
  ---
141
165
 
142
- ### diff-summary
143
-
144
- **Command** that displays a structured summary of git working tree changes (staged, unstaged, and untracked files). Injects git diff output directly into the prompt using bash commands.
145
-
146
- #### Usage
147
-
148
- ```bash
149
- /diff-summary
150
- ```
151
-
152
- #### What it shows
153
-
154
- - Git status (porcelain format)
155
- - Staged changes (stats and full diff)
156
- - Unstaged changes (stats and full diff)
157
- - Untracked files content (first 50 lines of each file)
158
-
159
- #### Implementation
160
-
161
- This command uses OpenCode's `!`\`...\`` syntax to inject bash command output directly into the prompt, avoiding the 2000-line truncation limit that affects tools.
162
-
163
- See `command/diff-summary.md` for the full implementation.
164
-
165
- #### Output Structure
166
-
167
- **For branch comparisons:**
168
- - Stats Overview: Summary of changes (insertions, deletions)
169
- - Commits to Review: List of commits in the range
170
- - Files Changed: List of modified files
171
- - Full Diff: Complete diff with context
172
-
173
- **For working tree changes:**
174
- - Status Overview: Git status output
175
- - Staged Changes: Stats, files, and diff for staged changes
176
- - Unstaged Changes: Stats, files, and diff for unstaged changes
177
- - Untracked Files: List and diffs for new untracked files
178
-
179
- #### Notes
180
-
181
- - When comparing branches, the tool fetches from the remote before generating the diff
182
- - Diffs include 5 lines of context and function context for better readability
183
-
184
- ---
185
-
186
166
  ### prompt-session
187
167
 
188
168
  Send a message to a child session (subagent) to continue the conversation. Useful for iterating with subagents without creating new sessions.
@@ -250,6 +230,46 @@ Child sessions (2):
250
230
 
251
231
  ---
252
232
 
233
+ ### agent-promote
234
+
235
+ Change the type of a plugin agent at runtime. Promotes subagents to primary agents (visible in Tab selection) or demotes them back.
236
+
237
+ #### Parameters
238
+
239
+ | Parameter | Type | Required | Description |
240
+ |-----------|------|----------|-------------|
241
+ | `name` | `string` | Yes | Name of the plugin agent (e.g., `rubber-duck`, `architect`) |
242
+ | `grade` | `string` | Yes | Target type: `subagent`, `primary`, or `all` |
243
+
244
+ #### Grade Types
245
+
246
+ | Grade | Effect |
247
+ |-------|--------|
248
+ | `subagent` | Available only as a subagent (default for most agents) |
249
+ | `primary` | Appears in Tab selection for direct use |
250
+ | `all` | Available both as primary and subagent |
251
+
252
+ #### Usage Examples
253
+
254
+ ```bash
255
+ # Promote rubber-duck to use it directly via Tab
256
+ /agent-promote rubber-duck primary
257
+
258
+ # Make architect available everywhere
259
+ /agent-promote architect all
260
+
261
+ # Revert code-reviewer to subagent only
262
+ /agent-promote code-reviewer subagent
263
+ ```
264
+
265
+ #### Notes
266
+
267
+ - Only agents from this plugin can be promoted (see [Agents](#agents) table)
268
+ - Changes persist in memory until OpenCode restarts
269
+ - After promotion, use `Tab` or `<leader>a` to select the agent
270
+
271
+ ---
272
+
253
273
  ### Blockchain
254
274
 
255
275
  Tools for querying Ethereum and EVM-compatible blockchains via Etherscan APIs.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Change the type of an agent to primary, subagent or all
3
+ ---
4
+
5
+ Use the agent-promote tool to change agent $1 to grade $2.
@@ -13,8 +13,13 @@ agent: build
13
13
  2. Present a summary to the user:
14
14
  - Files modified/added/deleted with stats
15
15
  - Proposed commit message based on the changes
16
- 3. Ask the user for confirmation before proceeding
17
- 4. Only if the user confirms:
16
+ 3. **If the current branch is `master`, `main`, `develop`, or `dev`:**
17
+ - Warn the user that committing directly to this branch is discouraged
18
+ - Propose to create a new feature branch with a suggested name based on the changes
19
+ - Ask the user if they want to: (a) create the suggested branch, (b) provide a custom branch name, or (c) continue on the current branch anyway
20
+ - If the user chooses to create a branch, create it and switch to it before proceeding
21
+ 4. Ask the user for confirmation before proceeding
22
+ 5. Only if the user confirms:
18
23
  - Stage all changes (`git add -A`)
19
24
  - Create the commit with the agreed message
20
25
  - Push to origin on the current branch
@@ -1,19 +1,51 @@
1
1
  ---
2
- description: Show working tree changes (staged, unstaged, untracked)
2
+ description: Show working tree changes or diff between branches ($1=source, $2=target)
3
3
  ---
4
4
 
5
- # Diff Summary: Working Tree → HEAD
5
+ # Diff Summary
6
6
 
7
- ## Status
8
- !`git status --porcelain`
7
+ !`bash -c '
8
+ SOURCE="$1"
9
+ TARGET="$2"
10
+ TARGET="${TARGET:-HEAD}"
9
11
 
10
- ## Staged Changes
11
- !`git diff --cached --stat`
12
- !`git diff --cached`
12
+ if [ -n "$SOURCE" ]; then
13
+ echo "## Branch Comparison: $SOURCE → $TARGET"
14
+ echo ""
15
+ git fetch --all --prune 2>/dev/null || true
16
+
17
+ echo "### Stats Overview"
18
+ git diff --stat "$TARGET"..."$SOURCE"
19
+
20
+ echo ""
21
+ echo "### Commits"
22
+ git log --oneline --no-merges "$TARGET".."$SOURCE"
23
+
24
+ echo ""
25
+ echo "### Files Changed"
26
+ git diff --name-only "$TARGET"..."$SOURCE"
27
+
28
+ echo ""
29
+ echo "### Full Diff"
30
+ git diff "$TARGET"..."$SOURCE"
31
+ else
32
+ echo "## Status"
33
+ git status --porcelain
13
34
 
14
- ## Unstaged Changes
15
- !`git diff --stat`
16
- !`git diff`
35
+ echo ""
36
+ echo "## Staged Changes"
37
+ git diff --cached --stat
38
+ git diff --cached
17
39
 
18
- ## Untracked Files Content
19
- !`bash -c 'git ls-files --others --exclude-standard | while read f; do [ -f "$f" ] && echo "=== $f ===" && sed -n "1,50p" "$f" && sed -n "51p" "$f" | grep -q . && echo "... (truncated)"; done'`
40
+ echo ""
41
+ echo "## Unstaged Changes"
42
+ git diff --stat
43
+ git diff
44
+
45
+ echo ""
46
+ echo "## Untracked Files Content"
47
+ git ls-files --others --exclude-standard | while read f; do
48
+ [ -f "$f" ] && echo "=== $f ===" && sed -n "1,50p" "$f" && sed -n "51p" "$f" | grep -q . && echo "... (truncated)"
49
+ done
50
+ fi
51
+ '`
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Commit, push and create a GitHub PR
3
+ agent: build
4
+ ---
5
+
6
+ ## Context
7
+
8
+ - Current branch: !`git branch --show-current`
9
+ - Default branch: !`gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'`
10
+
11
+ ## Your task
12
+
13
+ 1. Execute `/commit-push` to commit and push all changes
14
+ 2. Once the push is complete, create a PR using `gh pr create`:
15
+ - Use the commit message as PR title
16
+ - Generate a brief PR description summarizing the changes
17
+ - Target the repository's default branch
18
+ 3. Display the PR URL to the user
@@ -5,19 +5,6 @@ agent: code-reviewer
5
5
 
6
6
  # Review: $1 → $2
7
7
 
8
- ## Fetch latest
9
- !`git fetch --all --prune 2>/dev/null || true`
10
-
11
- ## Stats Overview
12
- !`git diff --stat $2...$1`
13
-
14
- ## Commits to Review
15
- !`git log --oneline --no-merges $2..$1`
16
-
17
- ## Files Changed
18
- !`git diff --name-only $2...$1`
19
-
20
- ## Full Diff
21
- !`git diff -U5 --function-context $2...$1`
8
+ /diff-summary $1 $2
22
9
 
23
10
  Review the above changes for quality, correctness, and adherence to project guidelines.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { getGlobalHookDir, getProjectHookDir } from "./config-paths";
5
5
  import { hasCodeExtension } from "./code-files";
6
6
  import { log } from "./logger";
7
7
  import { executeBashAction, DEFAULT_BASH_TIMEOUT, } from "./bash-executor";
8
- import { gitingestTool, createPromptSessionTool, createListChildSessionsTool, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
8
+ import { gitingestTool, createPromptSessionTool, createListChildSessionsTool, createAgentPromoteTool, getPromotedAgents, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
9
9
  export { parseFrontmatter, loadAgents, loadCommands } from "./loaders";
10
10
  // ============================================================================
11
11
  // CONSTANTS
@@ -32,6 +32,7 @@ const SmartfrogPlugin = async (ctx) => {
32
32
  hooks: Array.from(hooks.keys()),
33
33
  tools: [
34
34
  "gitingest",
35
+ "agent-promote",
35
36
  "eth-transaction",
36
37
  "eth-address-txs",
37
38
  "eth-address-balance",
@@ -173,8 +174,14 @@ const SmartfrogPlugin = async (ctx) => {
173
174
  }
174
175
  return {
175
176
  config: async (config) => {
176
- if (Object.keys(agents).length > 0) {
177
- config.agent = { ...(config.agent ?? {}), ...agents };
177
+ const loadedAgents = loadAgents(AGENT_DIR);
178
+ for (const [name, mode] of getPromotedAgents()) {
179
+ if (loadedAgents[name]) {
180
+ loadedAgents[name].mode = mode;
181
+ }
182
+ }
183
+ if (Object.keys(loadedAgents).length > 0) {
184
+ config.agent = { ...(config.agent ?? {}), ...loadedAgents };
178
185
  }
179
186
  if (Object.keys(commands).length > 0) {
180
187
  config.command = { ...(config.command ?? {}), ...commands };
@@ -184,6 +191,7 @@ const SmartfrogPlugin = async (ctx) => {
184
191
  gitingest: gitingestTool,
185
192
  "prompt-session": createPromptSessionTool(ctx.client),
186
193
  "list-child-sessions": createListChildSessionsTool(ctx.client),
194
+ "agent-promote": createAgentPromoteTool(ctx.client, Object.keys(agents)),
187
195
  "eth-transaction": ethTransactionTool,
188
196
  "eth-address-txs": ethAddressTxsTool,
189
197
  "eth-address-balance": ethAddressBalanceTool,
@@ -0,0 +1,6 @@
1
+ export type AgentMode = "subagent" | "primary" | "all";
2
+ export declare const VALID_GRADES: AgentMode[];
3
+ export declare function getPromotedAgents(): ReadonlyMap<string, AgentMode>;
4
+ export declare function setPromotedAgent(name: string, mode: AgentMode): void;
5
+ export declare function validateGrade(grade: string): grade is AgentMode;
6
+ export declare function validateAgentName(name: string, pluginAgentNames: string[]): boolean;
@@ -0,0 +1,14 @@
1
+ export const VALID_GRADES = ["subagent", "primary", "all"];
2
+ const promotedAgents = new Map();
3
+ export function getPromotedAgents() {
4
+ return promotedAgents;
5
+ }
6
+ export function setPromotedAgent(name, mode) {
7
+ promotedAgents.set(name, mode);
8
+ }
9
+ export function validateGrade(grade) {
10
+ return VALID_GRADES.includes(grade);
11
+ }
12
+ export function validateAgentName(name, pluginAgentNames) {
13
+ return pluginAgentNames.includes(name);
14
+ }
@@ -0,0 +1,19 @@
1
+ import { type ToolContext } from "@opencode-ai/plugin";
2
+ import type { createOpencodeClient } from "@opencode-ai/sdk";
3
+ export { type AgentMode, VALID_GRADES, getPromotedAgents, setPromotedAgent, validateGrade, validateAgentName, } from "./agent-promote-core";
4
+ type Client = ReturnType<typeof createOpencodeClient>;
5
+ export interface AgentPromoteArgs {
6
+ name: string;
7
+ grade: string;
8
+ }
9
+ export declare function createAgentPromoteTool(client: Client, pluginAgentNames: string[]): {
10
+ description: string;
11
+ args: {
12
+ name: import("zod").ZodString;
13
+ grade: import("zod").ZodString;
14
+ };
15
+ execute(args: {
16
+ name: string;
17
+ grade: string;
18
+ }, context: ToolContext): Promise<string>;
19
+ };
@@ -0,0 +1,39 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { log } from "../logger";
3
+ import { VALID_GRADES, setPromotedAgent, validateGrade, validateAgentName, } from "./agent-promote-core";
4
+ export { VALID_GRADES, getPromotedAgents, setPromotedAgent, validateGrade, validateAgentName, } from "./agent-promote-core";
5
+ export function createAgentPromoteTool(client, pluginAgentNames) {
6
+ return tool({
7
+ description: "Change the type of an agent to primary, subagent or all",
8
+ args: {
9
+ name: tool.schema.string().describe("Name of the agent"),
10
+ grade: tool.schema.string().describe("Target type: 'subagent', 'primary', or 'all'"),
11
+ },
12
+ async execute(args, _context) {
13
+ const { name, grade } = args;
14
+ if (!validateGrade(grade)) {
15
+ return `Invalid grade "${grade}". Valid grades: ${VALID_GRADES.join(", ")}`;
16
+ }
17
+ if (!validateAgentName(name, pluginAgentNames)) {
18
+ return `Agent "${name}" not found in this plugin. Available: ${pluginAgentNames.join(", ")}`;
19
+ }
20
+ const agentsResp = await client.app.agents();
21
+ const agents = agentsResp.data ?? [];
22
+ const existingAgent = agents.find((a) => a.name === name);
23
+ if (existingAgent && existingAgent.mode === grade) {
24
+ return `Agent "${name}" is already of type "${grade}"`;
25
+ }
26
+ setPromotedAgent(name, grade);
27
+ log("[agent-promote] Agent type changed", { name, grade });
28
+ await client.tui.showToast({
29
+ body: {
30
+ message: `Promoting agent "${name}" to "${grade}"...`,
31
+ variant: "success",
32
+ duration: 3000,
33
+ },
34
+ });
35
+ await client.instance.dispose();
36
+ return `Agent "${name}" changed to type "${grade}". Use Tab or <leader>a to select it.`;
37
+ },
38
+ });
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateGrade, validateAgentName, getPromotedAgents, setPromotedAgent, VALID_GRADES, } from "./agent-promote-core";
3
+ describe("agent-promote", () => {
4
+ const pluginAgentNames = ["rubber-duck", "architect", "code-reviewer"];
5
+ describe("VALID_GRADES", () => {
6
+ it("should contain subagent, primary, and all", () => {
7
+ expect(VALID_GRADES).toContain("subagent");
8
+ expect(VALID_GRADES).toContain("primary");
9
+ expect(VALID_GRADES).toContain("all");
10
+ expect(VALID_GRADES).toHaveLength(3);
11
+ });
12
+ });
13
+ describe("validateGrade", () => {
14
+ it("should return true for valid grade: subagent", () => {
15
+ expect(validateGrade("subagent")).toBe(true);
16
+ });
17
+ it("should return true for valid grade: primary", () => {
18
+ expect(validateGrade("primary")).toBe(true);
19
+ });
20
+ it("should return true for valid grade: all", () => {
21
+ expect(validateGrade("all")).toBe(true);
22
+ });
23
+ it("should return false for invalid grade", () => {
24
+ expect(validateGrade("invalid")).toBe(false);
25
+ expect(validateGrade("foo")).toBe(false);
26
+ expect(validateGrade("")).toBe(false);
27
+ });
28
+ });
29
+ describe("validateAgentName", () => {
30
+ it("should return true for agent in plugin", () => {
31
+ expect(validateAgentName("rubber-duck", pluginAgentNames)).toBe(true);
32
+ expect(validateAgentName("architect", pluginAgentNames)).toBe(true);
33
+ expect(validateAgentName("code-reviewer", pluginAgentNames)).toBe(true);
34
+ });
35
+ it("should return false for agent not in plugin", () => {
36
+ expect(validateAgentName("unknown", pluginAgentNames)).toBe(false);
37
+ expect(validateAgentName("build", pluginAgentNames)).toBe(false);
38
+ expect(validateAgentName("", pluginAgentNames)).toBe(false);
39
+ });
40
+ });
41
+ describe("promotedAgents Map", () => {
42
+ it("should set and get promoted agent", () => {
43
+ setPromotedAgent("test-agent-1", "primary");
44
+ const promoted = getPromotedAgents();
45
+ expect(promoted.get("test-agent-1")).toBe("primary");
46
+ });
47
+ it("should update existing promotion", () => {
48
+ setPromotedAgent("test-agent-2", "primary");
49
+ expect(getPromotedAgents().get("test-agent-2")).toBe("primary");
50
+ setPromotedAgent("test-agent-2", "all");
51
+ expect(getPromotedAgents().get("test-agent-2")).toBe("all");
52
+ setPromotedAgent("test-agent-2", "subagent");
53
+ expect(getPromotedAgents().get("test-agent-2")).toBe("subagent");
54
+ });
55
+ it("should handle multiple agents", () => {
56
+ setPromotedAgent("agent-a", "primary");
57
+ setPromotedAgent("agent-b", "all");
58
+ setPromotedAgent("agent-c", "subagent");
59
+ const promoted = getPromotedAgents();
60
+ expect(promoted.get("agent-a")).toBe("primary");
61
+ expect(promoted.get("agent-b")).toBe("all");
62
+ expect(promoted.get("agent-c")).toBe("subagent");
63
+ });
64
+ it("should return readonly map", () => {
65
+ const promoted = getPromotedAgents();
66
+ expect(typeof promoted.get).toBe("function");
67
+ expect(typeof promoted.has).toBe("function");
68
+ expect(typeof promoted.forEach).toBe("function");
69
+ });
70
+ });
71
+ });
@@ -1,4 +1,5 @@
1
1
  export { gitingestTool, fetchGitingest, type GitingestArgs } from "./gitingest";
2
2
  export { createPromptSessionTool, type PromptSessionArgs } from "./prompt-session";
3
3
  export { createListChildSessionsTool } from "./list-child-sessions";
4
+ export { createAgentPromoteTool, getPromotedAgents, type AgentPromoteArgs } from "./agent-promote";
4
5
  export { ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, EtherscanClient, EtherscanClientError, weiToEth, formatTimestamp, shortenAddress, type EthTransactionArgs, type EthAddressTxsArgs, type EthAddressBalanceArgs, type EthTokenTransfersArgs, } from "./blockchain";
@@ -1,4 +1,5 @@
1
1
  export { gitingestTool, fetchGitingest } from "./gitingest";
2
2
  export { createPromptSessionTool } from "./prompt-session";
3
3
  export { createListChildSessionsTool } from "./list-child-sessions";
4
+ export { createAgentPromoteTool, getPromotedAgents } from "./agent-promote";
4
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.4.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",