opencode-froggy 0.4.0 → 0.5.1
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 +100 -51
- package/command/agent-demote.md +5 -0
- package/command/agent-promote.md +5 -0
- package/command/commit-push.md +7 -2
- package/command/diff-summary.md +44 -12
- package/command/gh-create-pr.md +18 -0
- package/command/send-to.md +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +33 -4
- package/dist/index.test.js +107 -0
- package/dist/loaders.d.ts +2 -0
- package/dist/loaders.js +1 -0
- package/dist/skill-activation.d.ts +2 -0
- package/dist/skill-activation.js +11 -0
- package/dist/tools/agent-promote-core.d.ts +6 -0
- package/dist/tools/agent-promote-core.js +14 -0
- package/dist/tools/agent-promote.d.ts +19 -0
- package/dist/tools/agent-promote.js +40 -0
- package/dist/tools/agent-promote.test.d.ts +1 -0
- package/dist/tools/agent-promote.test.js +71 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/skill.d.ts +21 -0
- package/dist/tools/skill.js +151 -0
- package/dist/tools/skill.test.d.ts +1 -0
- package/dist/tools/skill.test.js +231 -0
- package/package.json +1 -1
- package/skill/code-review/SKILL.md +116 -0
- package/skill/code-simplify/SKILL.md +76 -2
- package/agent/code-reviewer.md +0 -89
- package/agent/code-simplifier.md +0 -77
- package/command/review-changes.md +0 -22
- package/command/review-pr.md +0 -23
- package/command/simplify-changes.md +0 -8
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<a href="https://www.npmjs.com/package/opencode-froggy"><img src="https://badge.fury.io/js/opencode-froggy.svg" alt="npm version"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
OpenCode plugin providing hooks, specialized agents (architect, doc-writer, rubber-duck, partner), skills (code-review, code-simplify), and tools (gitingest, blockchain queries, agent-promote).
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -16,11 +16,14 @@ Plugin providing Claude Code–style hooks, specialized agents (doc-writer, code
|
|
|
16
16
|
- [Installation](#installation)
|
|
17
17
|
- [Commands](#commands)
|
|
18
18
|
- [Agents](#agents)
|
|
19
|
+
- [Skills](#skills)
|
|
20
|
+
- [code-review](#code-review)
|
|
21
|
+
- [code-simplify](#code-simplify)
|
|
19
22
|
- [Tools](#tools)
|
|
20
23
|
- [gitingest](#gitingest)
|
|
21
|
-
- [diff-summary](#diff-summary)
|
|
22
24
|
- [prompt-session](#prompt-session)
|
|
23
25
|
- [list-child-sessions](#list-child-sessions)
|
|
26
|
+
- [agent-promote](#agent-promote)
|
|
24
27
|
- [Blockchain](#blockchain)
|
|
25
28
|
- [Configuration](#configuration)
|
|
26
29
|
- [eth-transaction](#eth-transaction)
|
|
@@ -66,14 +69,34 @@ Alternatively, clone or copy the plugin files to one of these directories:
|
|
|
66
69
|
|
|
67
70
|
| Command | Description | Agent |
|
|
68
71
|
|---------|-------------|-------|
|
|
72
|
+
| `/agent-promote <name> [grade]` | Promote an agent to primary (default) or specify grade: `subagent`, `primary`, `all` | - |
|
|
73
|
+
| `/agent-demote <name>` | Demote an agent to subagent | - |
|
|
69
74
|
| `/commit-push` | Stage, commit, and push changes with user confirmation | `build` |
|
|
75
|
+
| `/diff-summary [source] [target]` | Show working tree changes or diff between branches | - |
|
|
70
76
|
| `/doc-changes` | Update documentation based on uncommitted changes (new features only) | `doc-writer` |
|
|
71
|
-
| `/review-changes` | Review uncommitted changes (staged + unstaged, including untracked files) | `code-reviewer` |
|
|
72
|
-
| `/review-pr <source> <target>` | Review changes from source branch into target branch | `code-reviewer` |
|
|
73
77
|
| `/send-to [agent] <message>` | Send a message to a child session (subagent) to continue the conversation | - |
|
|
74
|
-
| `/simplify-changes` | Simplify uncommitted changes (staged + unstaged, including untracked files) | `code-simplifier` |
|
|
75
78
|
| `/tests-coverage` | Run the full test suite with coverage report and suggest fixes for failures | `build` |
|
|
76
79
|
|
|
80
|
+
### /diff-summary
|
|
81
|
+
|
|
82
|
+
The `/diff-summary` command supports two modes:
|
|
83
|
+
|
|
84
|
+
**Working tree mode** (no parameters):
|
|
85
|
+
```bash
|
|
86
|
+
/diff-summary
|
|
87
|
+
```
|
|
88
|
+
Shows staged changes, unstaged changes, and untracked file contents.
|
|
89
|
+
|
|
90
|
+
**Branch comparison mode** (with parameters):
|
|
91
|
+
```bash
|
|
92
|
+
# Compare a branch with the current branch (HEAD)
|
|
93
|
+
/diff-summary feature-branch
|
|
94
|
+
|
|
95
|
+
# Compare two specific branches
|
|
96
|
+
/diff-summary feature-branch main
|
|
97
|
+
```
|
|
98
|
+
Shows stats overview, commits, files changed, and full diff between branches.
|
|
99
|
+
|
|
77
100
|
---
|
|
78
101
|
|
|
79
102
|
## Agents
|
|
@@ -81,14 +104,44 @@ Alternatively, clone or copy the plugin files to one of these directories:
|
|
|
81
104
|
| Agent | Mode | Description |
|
|
82
105
|
|-------|------|-------------|
|
|
83
106
|
| `architect` | subagent | Strategic technical advisor providing high-leverage guidance on architecture, code structure, and complex engineering trade-offs. Read-only. |
|
|
84
|
-
| `code-reviewer` | subagent | Reviews code for quality, correctness, and security. Read-only with restricted git access. |
|
|
85
|
-
| `code-simplifier` | subagent | Simplifies recently modified code for clarity and maintainability while strictly preserving behavior. |
|
|
86
107
|
| `doc-writer` | subagent | Technical writer that crafts clear, comprehensive documentation (README, API docs, architecture docs, user guides). |
|
|
87
108
|
| `partner` | subagent | Strategic ideation partner that breaks frames, expands solution spaces, and surfaces non-obvious strategic options. Read-only. |
|
|
88
109
|
| `rubber-duck` | subagent | Strategic thinking partner for exploratory dialogue. Challenges assumptions, asks pointed questions, and sharpens thinking through conversational friction. Read-only. |
|
|
89
110
|
|
|
90
111
|
---
|
|
91
112
|
|
|
113
|
+
## Skills
|
|
114
|
+
|
|
115
|
+
Skills are loaded on-demand to provide specialized capabilities during a session.
|
|
116
|
+
|
|
117
|
+
### code-review
|
|
118
|
+
|
|
119
|
+
Read-only code review skill that analyzes diffs for real problems.
|
|
120
|
+
|
|
121
|
+
- **Purpose**: Provide actionable feedback on code changes without modifying files
|
|
122
|
+
- **Activation**: On user request, or before committing/merging changes
|
|
123
|
+
- **Focus areas**:
|
|
124
|
+
- Logic & stability (edge cases, race conditions, state transitions)
|
|
125
|
+
- Security (injection risks, validation, sensitive data exposure)
|
|
126
|
+
- Performance (resource leaks, O(n²) operations, unnecessary calls)
|
|
127
|
+
- Maintainability (SOLID violations, excessive complexity)
|
|
128
|
+
- **Output**: Numbered blocking issues with evidence and fix suggestions, plus optional simplification candidates
|
|
129
|
+
|
|
130
|
+
### code-simplify
|
|
131
|
+
|
|
132
|
+
Automatic simplification skill that improves code clarity while preserving behavior.
|
|
133
|
+
|
|
134
|
+
- **Purpose**: Ensure newly written code enters the codebase at a high standard of clarity
|
|
135
|
+
- **Activation**: Automatically after any code modification (new files, edits, refactors)
|
|
136
|
+
- **Core principle**: Behavior preservation is absolute — no changes to APIs, return values, side effects, or execution order
|
|
137
|
+
- **Focus areas**:
|
|
138
|
+
- Reduce unnecessary nesting and redundant checks
|
|
139
|
+
- Improve naming only when it prevents misunderstanding
|
|
140
|
+
- Consolidate related logic without merging concerns
|
|
141
|
+
- Remove comments that restate obvious code
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
92
145
|
## Tools
|
|
93
146
|
|
|
94
147
|
### gitingest
|
|
@@ -139,50 +192,6 @@ gitingest({
|
|
|
139
192
|
|
|
140
193
|
---
|
|
141
194
|
|
|
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
195
|
### prompt-session
|
|
187
196
|
|
|
188
197
|
Send a message to a child session (subagent) to continue the conversation. Useful for iterating with subagents without creating new sessions.
|
|
@@ -250,6 +259,46 @@ Child sessions (2):
|
|
|
250
259
|
|
|
251
260
|
---
|
|
252
261
|
|
|
262
|
+
### agent-promote
|
|
263
|
+
|
|
264
|
+
Promote an agent to primary (default) or specify a grade.
|
|
265
|
+
|
|
266
|
+
#### Parameters
|
|
267
|
+
|
|
268
|
+
| Parameter | Type | Required | Description |
|
|
269
|
+
|-----------|------|----------|-------------|
|
|
270
|
+
| `name` | `string` | Yes | Name of the plugin agent (e.g., `rubber-duck`, `architect`) |
|
|
271
|
+
| `grade` | `string` | No | Target type: `subagent`, `primary`, or `all` (default: `primary`) |
|
|
272
|
+
|
|
273
|
+
#### Grade Types
|
|
274
|
+
|
|
275
|
+
| Grade | Effect |
|
|
276
|
+
|-------|--------|
|
|
277
|
+
| `subagent` | Available only as a subagent |
|
|
278
|
+
| `primary` | Appears in Tab selection for direct use |
|
|
279
|
+
| `all` | Available both as primary and subagent |
|
|
280
|
+
|
|
281
|
+
#### Usage Examples
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Promote rubber-duck to primary (default)
|
|
285
|
+
/agent-promote rubber-duck
|
|
286
|
+
|
|
287
|
+
# Promote with explicit grade
|
|
288
|
+
/agent-promote architect all
|
|
289
|
+
|
|
290
|
+
# Demote back to subagent
|
|
291
|
+
/agent-demote rubber-duck
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Notes
|
|
295
|
+
|
|
296
|
+
- Only agents from this plugin can be promoted (see [Agents](#agents) table)
|
|
297
|
+
- Changes persist in memory until OpenCode restarts
|
|
298
|
+
- After promotion, use `Tab` or `<leader>a` to select the agent
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
253
302
|
### Blockchain
|
|
254
303
|
|
|
255
304
|
Tools for querying Ethereum and EVM-compatible blockchains via Etherscan APIs.
|
package/command/commit-push.md
CHANGED
|
@@ -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.
|
|
17
|
-
|
|
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
|
package/command/diff-summary.md
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Show working tree changes (
|
|
2
|
+
description: Show working tree changes or diff between branches ($1=source, $2=target)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
# Diff Summary
|
|
5
|
+
# Diff Summary
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
!`bash -c '
|
|
8
|
+
SOURCE="$1"
|
|
9
|
+
TARGET="$2"
|
|
10
|
+
TARGET="${TARGET:-HEAD}"
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
35
|
+
echo ""
|
|
36
|
+
echo "## Staged Changes"
|
|
37
|
+
git diff --cached --stat
|
|
38
|
+
git diff --cached
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
package/command/send-to.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Send a message to a child session (subagent) to continue the conver
|
|
|
5
5
|
Send a message to a child session using the `prompt-session` tool.
|
|
6
6
|
|
|
7
7
|
Parse the input:
|
|
8
|
-
- If the first word matches a known agent type (e.g., `rubber-duck`, `explore`, `
|
|
8
|
+
- If the first word matches a known agent type (e.g., `rubber-duck`, `explore`, `general`, `architect`, `doc-writer`, `partner`), use it to find the matching session and send the rest as the message
|
|
9
9
|
- Otherwise, send the entire input to the most recent child session
|
|
10
10
|
|
|
11
11
|
Steps:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
export { parseFrontmatter, loadAgents, loadCommands } from "./loaders";
|
|
2
|
+
export { parseFrontmatter, loadAgents, loadCommands, type LoadedSkill } from "./loaders";
|
|
3
|
+
export { buildSkillActivationBlock } from "./skill-activation";
|
|
3
4
|
declare const SmartfrogPlugin: Plugin;
|
|
4
5
|
export default SmartfrogPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { loadAgents, loadCommands, loadHooks, mergeHooks, } from "./loaders";
|
|
3
|
+
import { loadAgents, loadCommands, loadHooks, loadSkills, mergeHooks, } from "./loaders";
|
|
4
4
|
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, createSkillTool, getPromotedAgents, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
|
|
9
9
|
export { parseFrontmatter, loadAgents, loadCommands } from "./loaders";
|
|
10
|
+
export { buildSkillActivationBlock } from "./skill-activation";
|
|
11
|
+
import { buildSkillActivationBlock } from "./skill-activation";
|
|
10
12
|
// ============================================================================
|
|
11
13
|
// CONSTANTS
|
|
12
14
|
// ============================================================================
|
|
@@ -15,23 +17,37 @@ const __dirname = dirname(__filename);
|
|
|
15
17
|
const PLUGIN_ROOT = join(__dirname, "..");
|
|
16
18
|
const AGENT_DIR = join(PLUGIN_ROOT, "agent");
|
|
17
19
|
const COMMAND_DIR = join(PLUGIN_ROOT, "command");
|
|
20
|
+
const SKILL_DIR = join(PLUGIN_ROOT, "skill");
|
|
18
21
|
// ============================================================================
|
|
19
22
|
// PLUGIN
|
|
20
23
|
// ============================================================================
|
|
21
24
|
const SmartfrogPlugin = async (ctx) => {
|
|
22
25
|
const agents = loadAgents(AGENT_DIR);
|
|
23
26
|
const commands = loadCommands(COMMAND_DIR);
|
|
27
|
+
const skills = loadSkills(SKILL_DIR);
|
|
24
28
|
const globalHooks = loadHooks(getGlobalHookDir());
|
|
25
29
|
const projectHooks = loadHooks(getProjectHookDir(ctx.directory));
|
|
26
30
|
const hooks = mergeHooks(globalHooks, projectHooks);
|
|
27
31
|
const modifiedCodeFiles = new Map();
|
|
28
32
|
const pendingToolArgs = new Map();
|
|
33
|
+
const skillsWithTriggers = skills.filter(s => s.useWhen);
|
|
34
|
+
const skillActivationBlock = skillsWithTriggers.length > 0
|
|
35
|
+
? buildSkillActivationBlock(skillsWithTriggers)
|
|
36
|
+
: null;
|
|
37
|
+
const skillTool = createSkillTool({
|
|
38
|
+
pluginSkills: skills,
|
|
39
|
+
pluginDir: PLUGIN_ROOT,
|
|
40
|
+
});
|
|
29
41
|
log("[init] Plugin loaded", {
|
|
30
42
|
agents: Object.keys(agents),
|
|
31
43
|
commands: Object.keys(commands),
|
|
44
|
+
skills: skills.map(s => s.name),
|
|
45
|
+
skillsWithTriggers: skillsWithTriggers.map(s => s.name),
|
|
32
46
|
hooks: Array.from(hooks.keys()),
|
|
33
47
|
tools: [
|
|
34
48
|
"gitingest",
|
|
49
|
+
"skill",
|
|
50
|
+
"agent-promote",
|
|
35
51
|
"eth-transaction",
|
|
36
52
|
"eth-address-txs",
|
|
37
53
|
"eth-address-balance",
|
|
@@ -173,8 +189,14 @@ const SmartfrogPlugin = async (ctx) => {
|
|
|
173
189
|
}
|
|
174
190
|
return {
|
|
175
191
|
config: async (config) => {
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
const loadedAgents = loadAgents(AGENT_DIR);
|
|
193
|
+
for (const [name, mode] of getPromotedAgents()) {
|
|
194
|
+
if (loadedAgents[name]) {
|
|
195
|
+
loadedAgents[name].mode = mode;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (Object.keys(loadedAgents).length > 0) {
|
|
199
|
+
config.agent = { ...(config.agent ?? {}), ...loadedAgents };
|
|
178
200
|
}
|
|
179
201
|
if (Object.keys(commands).length > 0) {
|
|
180
202
|
config.command = { ...(config.command ?? {}), ...commands };
|
|
@@ -182,8 +204,10 @@ const SmartfrogPlugin = async (ctx) => {
|
|
|
182
204
|
},
|
|
183
205
|
tool: {
|
|
184
206
|
gitingest: gitingestTool,
|
|
207
|
+
skill: skillTool,
|
|
185
208
|
"prompt-session": createPromptSessionTool(ctx.client),
|
|
186
209
|
"list-child-sessions": createListChildSessionsTool(ctx.client),
|
|
210
|
+
"agent-promote": createAgentPromoteTool(ctx.client, Object.keys(agents)),
|
|
187
211
|
"eth-transaction": ethTransactionTool,
|
|
188
212
|
"eth-address-txs": ethAddressTxsTool,
|
|
189
213
|
"eth-address-balance": ethAddressBalanceTool,
|
|
@@ -256,6 +280,11 @@ const SmartfrogPlugin = async (ctx) => {
|
|
|
256
280
|
await triggerHooks("session.idle", sessionID, { files: files ? Array.from(files) : [] });
|
|
257
281
|
}
|
|
258
282
|
},
|
|
283
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
284
|
+
if (!skillActivationBlock)
|
|
285
|
+
return;
|
|
286
|
+
output.system.push(skillActivationBlock);
|
|
287
|
+
},
|
|
259
288
|
};
|
|
260
289
|
};
|
|
261
290
|
export default SmartfrogPlugin;
|
package/dist/index.test.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { parseFrontmatter, loadAgents, loadSkills, loadCommands, loadHooks, mergeHooks, } from "./loaders";
|
|
6
6
|
import { executeBashAction } from "./bash-executor";
|
|
7
|
+
import { buildSkillActivationBlock } from "./skill-activation";
|
|
7
8
|
describe("parseFrontmatter", () => {
|
|
8
9
|
it("should parse valid frontmatter", () => {
|
|
9
10
|
const content = `---
|
|
@@ -209,6 +210,35 @@ Content`;
|
|
|
209
210
|
expect(result).toHaveLength(1);
|
|
210
211
|
expect(result[0].name).toBe("valid");
|
|
211
212
|
});
|
|
213
|
+
it("should load use_when field from frontmatter", () => {
|
|
214
|
+
const skillDir = join(testDir, "auto-skill");
|
|
215
|
+
mkdirSync(skillDir);
|
|
216
|
+
const skillContent = `---
|
|
217
|
+
name: auto-skill
|
|
218
|
+
description: An auto-triggered skill
|
|
219
|
+
use_when: After completing a task, call this skill
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
Skill instructions here.`;
|
|
223
|
+
writeFileSync(join(skillDir, "SKILL.md"), skillContent);
|
|
224
|
+
const result = loadSkills(testDir);
|
|
225
|
+
expect(result).toHaveLength(1);
|
|
226
|
+
expect(result[0].name).toBe("auto-skill");
|
|
227
|
+
expect(result[0].useWhen).toBe("After completing a task, call this skill");
|
|
228
|
+
});
|
|
229
|
+
it("should have undefined useWhen when not provided", () => {
|
|
230
|
+
const skillDir = join(testDir, "manual-skill");
|
|
231
|
+
mkdirSync(skillDir);
|
|
232
|
+
const skillContent = `---
|
|
233
|
+
name: manual-skill
|
|
234
|
+
description: A manual skill
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
Content`;
|
|
238
|
+
writeFileSync(join(skillDir, "SKILL.md"), skillContent);
|
|
239
|
+
const result = loadSkills(testDir);
|
|
240
|
+
expect(result[0].useWhen).toBeUndefined();
|
|
241
|
+
});
|
|
212
242
|
});
|
|
213
243
|
describe("loadCommands", () => {
|
|
214
244
|
let testDir;
|
|
@@ -771,6 +801,83 @@ describe("executeBashAction", () => {
|
|
|
771
801
|
expect(parsed.tool_args).toBeUndefined();
|
|
772
802
|
});
|
|
773
803
|
});
|
|
804
|
+
describe("buildSkillActivationBlock", () => {
|
|
805
|
+
it("should return empty string for undefined input", () => {
|
|
806
|
+
const result = buildSkillActivationBlock(undefined);
|
|
807
|
+
expect(result).toBe("");
|
|
808
|
+
});
|
|
809
|
+
it("should return empty string for empty array", () => {
|
|
810
|
+
const result = buildSkillActivationBlock([]);
|
|
811
|
+
expect(result).toBe("");
|
|
812
|
+
});
|
|
813
|
+
it("should generate imperative instruction with skill name", () => {
|
|
814
|
+
const skills = [
|
|
815
|
+
{
|
|
816
|
+
name: "code-review",
|
|
817
|
+
description: "Review code",
|
|
818
|
+
useWhen: "After writing code",
|
|
819
|
+
path: "/path/to/skill",
|
|
820
|
+
body: "",
|
|
821
|
+
},
|
|
822
|
+
];
|
|
823
|
+
const result = buildSkillActivationBlock(skills);
|
|
824
|
+
expect(result).toContain("MANDATORY");
|
|
825
|
+
expect(result).toContain('skill({ name: "code-review" })');
|
|
826
|
+
expect(result).toContain("After writing code");
|
|
827
|
+
});
|
|
828
|
+
it("should preserve quotes in trigger text", () => {
|
|
829
|
+
const skills = [
|
|
830
|
+
{
|
|
831
|
+
name: "test-skill",
|
|
832
|
+
description: "Test",
|
|
833
|
+
useWhen: 'Call when user says "help"',
|
|
834
|
+
path: "/path",
|
|
835
|
+
body: "",
|
|
836
|
+
},
|
|
837
|
+
];
|
|
838
|
+
const result = buildSkillActivationBlock(skills);
|
|
839
|
+
expect(result).toContain('"help"');
|
|
840
|
+
});
|
|
841
|
+
it("should collapse whitespace in trigger text", () => {
|
|
842
|
+
const skills = [
|
|
843
|
+
{
|
|
844
|
+
name: "multi-line",
|
|
845
|
+
description: "Test",
|
|
846
|
+
useWhen: "First line\nSecond line\nThird line",
|
|
847
|
+
path: "/path",
|
|
848
|
+
body: "",
|
|
849
|
+
},
|
|
850
|
+
];
|
|
851
|
+
const result = buildSkillActivationBlock(skills);
|
|
852
|
+
expect(result).toContain("First line Second line Third line");
|
|
853
|
+
});
|
|
854
|
+
it("should trim whitespace from trigger text", () => {
|
|
855
|
+
const skills = [
|
|
856
|
+
{
|
|
857
|
+
name: "whitespace",
|
|
858
|
+
description: "Test",
|
|
859
|
+
useWhen: " padded trigger ",
|
|
860
|
+
path: "/path",
|
|
861
|
+
body: "",
|
|
862
|
+
},
|
|
863
|
+
];
|
|
864
|
+
const result = buildSkillActivationBlock(skills);
|
|
865
|
+
expect(result).toContain("padded trigger");
|
|
866
|
+
expect(result).not.toContain(" padded");
|
|
867
|
+
});
|
|
868
|
+
it("should generate separate instructions for multiple skills", () => {
|
|
869
|
+
const skills = [
|
|
870
|
+
{ name: "skill-a", description: "A", useWhen: "Trigger A", path: "/a", body: "" },
|
|
871
|
+
{ name: "skill-b", description: "B", useWhen: "Trigger B", path: "/b", body: "" },
|
|
872
|
+
];
|
|
873
|
+
const result = buildSkillActivationBlock(skills);
|
|
874
|
+
expect(result).toContain('skill({ name: "skill-a" })');
|
|
875
|
+
expect(result).toContain('skill({ name: "skill-b" })');
|
|
876
|
+
expect(result).toContain("Trigger A");
|
|
877
|
+
expect(result).toContain("Trigger B");
|
|
878
|
+
expect(result.match(/MANDATORY/g)).toHaveLength(2);
|
|
879
|
+
});
|
|
880
|
+
});
|
|
774
881
|
describe("mergeHooks", () => {
|
|
775
882
|
it("should return empty map when merging empty maps", () => {
|
|
776
883
|
const result = mergeHooks(new Map(), new Map());
|
package/dist/loaders.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface AgentFrontmatter {
|
|
|
12
12
|
export interface SkillFrontmatter {
|
|
13
13
|
name: string;
|
|
14
14
|
description: string;
|
|
15
|
+
use_when?: string;
|
|
15
16
|
license?: string;
|
|
16
17
|
compatibility?: string;
|
|
17
18
|
metadata?: Record<string, string>;
|
|
@@ -32,6 +33,7 @@ export interface CommandConfig {
|
|
|
32
33
|
export interface LoadedSkill {
|
|
33
34
|
name: string;
|
|
34
35
|
description: string;
|
|
36
|
+
useWhen?: string;
|
|
35
37
|
path: string;
|
|
36
38
|
body: string;
|
|
37
39
|
}
|
package/dist/loaders.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function formatTrigger(text) {
|
|
2
|
+
return text.replace(/\s+/g, " ").trim();
|
|
3
|
+
}
|
|
4
|
+
function buildSkillInstruction(skill) {
|
|
5
|
+
return `MANDATORY: Call skill({ name: "${skill.name}" }) ${formatTrigger(skill.useWhen)}`;
|
|
6
|
+
}
|
|
7
|
+
export function buildSkillActivationBlock(skills) {
|
|
8
|
+
if (!Array.isArray(skills) || skills.length === 0)
|
|
9
|
+
return "";
|
|
10
|
+
return skills.map(buildSkillInstruction).join("\n\n");
|
|
11
|
+
}
|
|
@@ -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").ZodOptional<import("zod").ZodString>;
|
|
14
|
+
};
|
|
15
|
+
execute(args: {
|
|
16
|
+
name: string;
|
|
17
|
+
grade?: string | undefined;
|
|
18
|
+
}, context: ToolContext): Promise<string>;
|
|
19
|
+
};
|